From e32e8258264c296c9a86f603588e95f97077409d Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Thu, 17 Mar 2022 17:31:38 +0100 Subject: [PATCH 01/33] implement column spanning --- .../columns/ColumnSpanningDerived.js | 124 +++ .../columns/ColumnSpanningDerived.tsx | 124 +++ .../columns/ColumnSpanningDerived.tsx.preview | 13 + .../columns/ColumnSpanningFunction.js | 102 +++ .../columns/ColumnSpanningFunction.tsx | 102 +++ .../ColumnSpanningFunction.tsx.preview | 12 + .../data-grid/columns/ColumnSpanningNumber.js | 27 + .../columns/ColumnSpanningNumber.tsx | 27 + .../columns/ColumnSpanningNumber.tsx.preview | 12 + docs/data/data-grid/columns/columns.md | 66 +- docs/pages/api-docs/data-grid/grid-col-def.md | 1 + docs/pages/x/api/data-grid/grid-col-def.md | 1 + .../src/columns/commodities.columns.tsx | 3 +- .../src/columns/employees.columns.tsx | 3 +- .../DataGridPro/useDataGridProComponent.tsx | 2 + .../columnResize/useGridColumnResize.tsx | 19 +- .../tests/columnSpanning.DataGridPro.test.tsx | 261 ++++++ .../x-data-grid-pro/src/utils/domUtils.ts | 5 +- .../src/DataGrid/useDataGridComponent.tsx | 2 + .../x-data-grid/src/components/GridRow.tsx | 53 +- .../columnHeaders/useGridColumnHeaders.tsx | 60 +- .../features/columns/gridColumnsUtils.ts | 53 ++ .../features/columns/useGridColumnSpanning.ts | 142 +++ .../keyboard/useGridKeyboardNavigation.ts | 25 +- .../hooks/features/scroll/useGridScroll.ts | 26 +- .../virtualization/useGridVirtualScroller.tsx | 79 +- .../grid/x-data-grid/src/internals/index.ts | 1 + .../src/models/api/gridApiCommon.ts | 2 + .../src/models/api/gridColumnSpanning.ts | 31 + .../src/models/colDef/gridColDef.ts | 5 + .../src/models/gridColumnSpanning.ts | 16 + .../tests/columnSpanning.DataGrid.test.tsx | 852 ++++++++++++++++++ .../src/stories/grid-columns.stories.tsx | 171 ++++ 33 files changed, 2338 insertions(+), 84 deletions(-) create mode 100644 docs/data/data-grid/columns/ColumnSpanningDerived.js create mode 100644 docs/data/data-grid/columns/ColumnSpanningDerived.tsx create mode 100644 docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview create mode 100644 docs/data/data-grid/columns/ColumnSpanningFunction.js create mode 100644 docs/data/data-grid/columns/ColumnSpanningFunction.tsx create mode 100644 docs/data/data-grid/columns/ColumnSpanningFunction.tsx.preview create mode 100644 docs/data/data-grid/columns/ColumnSpanningNumber.js create mode 100644 docs/data/data-grid/columns/ColumnSpanningNumber.tsx create mode 100644 docs/data/data-grid/columns/ColumnSpanningNumber.tsx.preview create mode 100644 packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx create mode 100644 packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts create mode 100644 packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts create mode 100644 packages/grid/x-data-grid/src/models/gridColumnSpanning.ts create mode 100644 packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.js b/docs/data/data-grid/columns/ColumnSpanningDerived.js new file mode 100644 index 0000000000000..9021c16793d46 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.js @@ -0,0 +1,124 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGridPro } from '@mui/x-data-grid-pro'; + +const slotTimesLookup = { + 0: '09:00 - 10:00', + 1: '10:00 - 11:00', + 2: '11:00 - 12:00', + 3: '12:00 - 13:00', + 4: '13:00 - 14:00', +}; + +const rows = [ + { + id: 1, + day: 'Monday', + slots: ['Maths', 'English', 'English', 'Lab', 'Lab'], + }, + { + id: 2, + day: 'Tuesday', + slots: ['Chemistry', 'Chemistry', 'Chemistry', 'Physics', 'Maths'], + }, + { + id: 3, + day: 'Wednesday', + slots: ['Physics', 'Chemistry', 'Physics', 'Maths', 'Maths'], + }, +]; + +function slotColSpan({ row, field, value }) { + const index = Number(field); + let colSpan = 1; + let nextIndex = index + 1; + let nextValue = row.slots[nextIndex]; + while (nextValue === value) { + colSpan += 1; + nextIndex += 1; + nextValue = row.slots[nextIndex]; + } + return colSpan; +} + +const columns = [ + { + field: 'day', + headerName: 'Day', + }, + { + field: '0', + headerName: slotTimesLookup[0], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[0]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '1', + headerName: slotTimesLookup[1], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[1]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '2', + headerName: slotTimesLookup[2], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[2]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '3', + headerName: slotTimesLookup[3], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[3]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '4', + headerName: slotTimesLookup[4], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[4]; + }, + colSpan: slotColSpan, + flex: 1, + }, +]; + +export default function ColumnSpanningDerived() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx new file mode 100644 index 0000000000000..1d354b6c58d95 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx @@ -0,0 +1,124 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGridPro, GridColDef, GridCellParams } from '@mui/x-data-grid-pro'; + +const slotTimesLookup = { + 0: '09:00 - 10:00', + 1: '10:00 - 11:00', + 2: '11:00 - 12:00', + 3: '12:00 - 13:00', + 4: '13:00 - 14:00', +}; + +const rows = [ + { + id: 1, + day: 'Monday', + slots: ['Maths', 'English', 'English', 'Lab', 'Lab'], + }, + { + id: 2, + day: 'Tuesday', + slots: ['Chemistry', 'Chemistry', 'Chemistry', 'Physics', 'Maths'], + }, + { + id: 3, + day: 'Wednesday', + slots: ['Physics', 'Chemistry', 'Physics', 'Maths', 'Maths'], + }, +]; + +function slotColSpan({ row, field, value }: GridCellParams) { + const index = Number(field); + let colSpan = 1; + let nextIndex = index + 1; + let nextValue = row.slots[nextIndex]; + while (nextValue === value) { + colSpan += 1; + nextIndex += 1; + nextValue = row.slots[nextIndex]; + } + return colSpan; +} + +const columns: GridColDef[] = [ + { + field: 'day', + headerName: 'Day', + }, + { + field: '0', + headerName: slotTimesLookup[0], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[0]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '1', + headerName: slotTimesLookup[1], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[1]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '2', + headerName: slotTimesLookup[2], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[2]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '3', + headerName: slotTimesLookup[3], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[3]; + }, + colSpan: slotColSpan, + flex: 1, + }, + { + field: '4', + headerName: slotTimesLookup[4], + sortable: false, + filterable: false, + valueGetter: ({ row }) => { + return row.slots[4]; + }, + colSpan: slotColSpan, + flex: 1, + }, +]; + +export default function ColumnSpanningDerived() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview new file mode 100644 index 0000000000000..db2d0d5a44747 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.js b/docs/data/data-grid/columns/ColumnSpanningFunction.js new file mode 100644 index 0000000000000..01ec8f5aab43f --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.js @@ -0,0 +1,102 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGridPro } from '@mui/x-data-grid-pro'; + +const rows = [ + { id: 1, item: 'Paperclip', quantity: 100, price: 1.99 }, + { id: 2, item: 'Paper', quantity: 10, price: 30 }, + { id: 3, item: 'Pencil', quantity: 100, price: 1.25 }, + { id: 'SUBTOTAL', label: 'Subtotal', subtotal: 624 }, + { id: 'TAX', label: 'Tax', taxRate: 10, taxTotal: 62.4 }, + { id: 'TOTAL', label: 'Total', total: 686.4 }, +]; + +const columns = [ + { + field: 'item', + headerName: 'Item/Description', + flex: 3, + sortable: false, + colSpan: ({ row }) => { + if (row.id === 'SUBTOTAL' || row.id === 'TOTAL') { + return 3; + } + if (row.id === 'TAX') { + return 2; + } + return undefined; + }, + valueGetter: ({ value, row }) => { + if (row.id === 'SUBTOTAL' || row.id === 'TAX' || row.id === 'TOTAL') { + return row.label; + } + return value; + }, + }, + { field: 'quantity', headerName: 'Quantity', flex: 1, sortable: false }, + { + field: 'price', + headerName: 'Price', + flex: 1, + sortable: false, + valueGetter: ({ row, value }) => { + if (row.id === 'TAX') { + return `${row.taxRate}%`; + } + return value; + }, + }, + { + field: 'total', + headerName: 'Total', + flex: 1, + sortable: false, + valueGetter: ({ row }) => { + if (row.id === 'SUBTOTAL') { + return row.subtotal; + } + if (row.id === 'TAX') { + return row.taxTotal; + } + if (row.id === 'TOTAL') { + return row.total; + } + return row.price * row.quantity; + }, + }, +]; + +function getCellClassName({ row, field }) { + if (row.id === 'SUBTOTAL' || row.id === 'TOTAL' || row.id === 'TAX') { + if (field === 'item') { + return 'bold'; + } + } + return ''; +} + +export default function ColumnSpanningFunction() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.tsx b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx new file mode 100644 index 0000000000000..01ec8f5aab43f --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGridPro } from '@mui/x-data-grid-pro'; + +const rows = [ + { id: 1, item: 'Paperclip', quantity: 100, price: 1.99 }, + { id: 2, item: 'Paper', quantity: 10, price: 30 }, + { id: 3, item: 'Pencil', quantity: 100, price: 1.25 }, + { id: 'SUBTOTAL', label: 'Subtotal', subtotal: 624 }, + { id: 'TAX', label: 'Tax', taxRate: 10, taxTotal: 62.4 }, + { id: 'TOTAL', label: 'Total', total: 686.4 }, +]; + +const columns = [ + { + field: 'item', + headerName: 'Item/Description', + flex: 3, + sortable: false, + colSpan: ({ row }) => { + if (row.id === 'SUBTOTAL' || row.id === 'TOTAL') { + return 3; + } + if (row.id === 'TAX') { + return 2; + } + return undefined; + }, + valueGetter: ({ value, row }) => { + if (row.id === 'SUBTOTAL' || row.id === 'TAX' || row.id === 'TOTAL') { + return row.label; + } + return value; + }, + }, + { field: 'quantity', headerName: 'Quantity', flex: 1, sortable: false }, + { + field: 'price', + headerName: 'Price', + flex: 1, + sortable: false, + valueGetter: ({ row, value }) => { + if (row.id === 'TAX') { + return `${row.taxRate}%`; + } + return value; + }, + }, + { + field: 'total', + headerName: 'Total', + flex: 1, + sortable: false, + valueGetter: ({ row }) => { + if (row.id === 'SUBTOTAL') { + return row.subtotal; + } + if (row.id === 'TAX') { + return row.taxTotal; + } + if (row.id === 'TOTAL') { + return row.total; + } + return row.price * row.quantity; + }, + }, +]; + +function getCellClassName({ row, field }) { + if (row.id === 'SUBTOTAL' || row.id === 'TOTAL' || row.id === 'TAX') { + if (field === 'item') { + return 'bold'; + } + } + return ''; +} + +export default function ColumnSpanningFunction() { + return ( + + + + ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.tsx.preview b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx.preview new file mode 100644 index 0000000000000..7b304670ffd00 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx.preview @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/columns/ColumnSpanningNumber.js b/docs/data/data-grid/columns/ColumnSpanningNumber.js new file mode 100644 index 0000000000000..2f1885c8e8a24 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningNumber.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { DataGrid } from '@mui/x-data-grid'; + +const otherProps = { + autoHeight: true, + showCellRightBorder: true, + showColumnRightBorder: true, +}; + +export default function ColumnSpanningNumber() { + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningNumber.tsx b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx new file mode 100644 index 0000000000000..2f1885c8e8a24 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { DataGrid } from '@mui/x-data-grid'; + +const otherProps = { + autoHeight: true, + showCellRightBorder: true, + showColumnRightBorder: true, +}; + +export default function ColumnSpanningNumber() { + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/columns/ColumnSpanningNumber.tsx.preview b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx.preview new file mode 100644 index 0000000000000..2c8b32d329bd0 --- /dev/null +++ b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx.preview @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index 95155371f8412..339b10bb94b40 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -509,24 +509,68 @@ To pin the checkbox column added when using `checkboxSelection`, add `GRID_CHECK {{"demo": "ColumnPinningApiNoSnap.js", "bg": "inline", "hideToolbar": true}} -## 🚧 Column groups +## Column spanning -> ⚠️ This feature isn't implemented yet. It's coming. -> -> 👍 Upvote [issue #195](https://github.com/mui/mui-x/issues/195) if you want to see it land faster. +Each cell takes up the width of one column. +You can modify this default behavior with column spanning. +It allows cells to span multiple columns. +This is very close to the "column spanning" in an HTML ``. -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. +To change the number of columns a cell should span, use the `colSpan` property available in `GridColDef`: -## 🚧 Column spanning +```ts +interface GridColDef { + /** + * Number of columns a grid cell should span. + * @default 1 + */ + colSpan?: number | ((params: GridCellParams) => number | undefined); + … +} +``` + +> When using `colSpan`, sorting and filtering might not work as expected. +> Make sure to disable [sorting](/components/data-grid/sorting/#disable-the-sorting) and [filtering](/components/data-grid/filtering/#disable-the-filters) for the column(s) that are affected by `colSpan`. + + + +> While [column reorder](/components/data-grid/columns/#column-reorder) works with `colSpan`, disabling it can be useful to avoid confusing grid layout. + +### Number signature + +The `colSpan` number signature allows to span all the cells in the column: + +```ts +interface GridColDef { + colSpan?: number; +} +``` + +{{"demo": "ColumnSpanningNumber.js", "bg": "inline"}} + +### Function signature + +The `colSpan` function signature is useful for spanning only specific cells in the column. The function receives [`GridCellParams`](/api/data-grid/grid-cell-params/) as the first argument: + +```ts +interface GridColDef { + colSpan?: (params: GridCellParams) => number | undefined; +} +``` + +{{"demo": "ColumnSpanningFunction.js", "bg": "inline", "defaultCodeOpen": false}} + +Function signature can also be useful to derive `colSpan` value from row data: + +{{"demo": "ColumnSpanningDerived.js", "bg": "inline", "defaultCodeOpen": false}} + +## 🚧 Column groups > ⚠️ This feature isn't implemented yet. It's coming. > -> 👍 Upvote [issue #192](https://github.com/mui/mui-x/issues/192) if you want to see it land faster. +> 👍 Upvote [issue #195](https://github.com/mui/mui-x/issues/195) if you want to see it land faster. -Each cell takes up the width of one column. -You can modify this default behavior with column spanning. -It allows cells to span multiple columns. -This is very close to the "column spanning" in an HTML `
`. +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. ## Selectors [](https://mui.com/store/items/material-ui-pro/) diff --git a/docs/pages/api-docs/data-grid/grid-col-def.md b/docs/pages/api-docs/data-grid/grid-col-def.md index d9e875680a298..959803f4e1b2f 100644 --- a/docs/pages/api-docs/data-grid/grid-col-def.md +++ b/docs/pages/api-docs/data-grid/grid-col-def.md @@ -16,6 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | +| colSpan? | number \| ((params: GridCellParams) => number \| undefined) | 1 | Number of columns a grid cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | diff --git a/docs/pages/x/api/data-grid/grid-col-def.md b/docs/pages/x/api/data-grid/grid-col-def.md index d9e875680a298..959803f4e1b2f 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.md +++ b/docs/pages/x/api/data-grid/grid-col-def.md @@ -16,6 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | +| colSpan? | number \| ((params: GridCellParams) => number \| undefined) | 1 | Number of columns a grid cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | diff --git a/packages/grid/x-data-grid-generator/src/columns/commodities.columns.tsx b/packages/grid/x-data-grid-generator/src/columns/commodities.columns.tsx index f2e149d7a15c7..be2fb663f7944 100644 --- a/packages/grid/x-data-grid-generator/src/columns/commodities.columns.tsx +++ b/packages/grid/x-data-grid-generator/src/columns/commodities.columns.tsx @@ -244,7 +244,8 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => return value; }, - valueFormatter: ({ value }) => (value as typeof COUNTRY_ISO_OPTIONS_SORTED[number])?.label, + valueFormatter: ({ value }) => + value ? (value as typeof COUNTRY_ISO_OPTIONS_SORTED[number]).label : '', groupingValueGetter: (params) => params.value.code, sortComparator: (v1, v2, param1, param2) => gridStringOrNumberComparator( diff --git a/packages/grid/x-data-grid-generator/src/columns/employees.columns.tsx b/packages/grid/x-data-grid-generator/src/columns/employees.columns.tsx index 00e93c28dcb7b..9ebb57707265f 100644 --- a/packages/grid/x-data-grid-generator/src/columns/employees.columns.tsx +++ b/packages/grid/x-data-grid-generator/src/columns/employees.columns.tsx @@ -103,7 +103,8 @@ export const getEmployeeColumns = (): GridColDefGenerator[] => [ headerName: 'Country', type: 'singleSelect', valueOptions: COUNTRY_ISO_OPTIONS_SORTED, - valueFormatter: ({ value }) => (value as typeof COUNTRY_ISO_OPTIONS_SORTED[number])?.label, + valueFormatter: ({ value }) => + value ? (value as typeof COUNTRY_ISO_OPTIONS_SORTED[number]).label : '', generateData: randomCountry, renderCell: renderCountry, renderEditCell: renderEditCountry, 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 7e00b82917d4d..8c5220bac918b 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -34,6 +34,7 @@ import { useGridDimensions, useGridStatePersistence, useGridSelectionPreProcessors, + useGridColumnSpanning, columnMenuStateInitializer, densityStateInitializer, focusStateInitializer, @@ -127,6 +128,7 @@ export const useDataGridProComponent = ( useGridColumns(apiRef, props); useGridRows(apiRef, props); useGridParamsApi(apiRef); + useGridColumnSpanning(apiRef); useGridDetailPanelCache(apiRef, props); const useGridEditing = props.experimentalFeatures?.newEditingApi 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 bf86d9d1b9a74..3830c1b18a6a2 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 @@ -134,6 +134,9 @@ export const useGridColumnResize = ( const updateWidth = (newWidth: number) => { logger.debug(`Updating width to ${newWidth} for col ${colDefRef.current!.field}`); + const prevWidth = colElementRef.current!.offsetWidth; + const widthDiff = newWidth - prevWidth; + colDefRef.current!.computedWidth = newWidth; colDefRef.current!.width = newWidth; colDefRef.current!.flex = undefined; @@ -144,9 +147,19 @@ export const useGridColumnResize = ( colCellElementsRef.current!.forEach((element) => { const div = element as HTMLDivElement; - div.style.width = `${newWidth}px`; - div.style.minWidth = `${newWidth}px`; - div.style.maxWidth = `${newWidth}px`; + let finalWidth: `${number}px`; + + if (div.getAttribute('colspan') === '1') { + finalWidth = `${newWidth}px`; + } else { + // Cell with colspan > 1 cannot be just updated width new width. + // Instead, we add width diff to the current width. + finalWidth = `${div.offsetWidth + widthDiff}px`; + } + + div.style.width = finalWidth; + div.style.minWidth = finalWidth; + div.style.maxWidth = finalWidth; }); }; diff --git a/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx new file mode 100644 index 0000000000000..c0232dd78cc6f --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx @@ -0,0 +1,261 @@ +import * as React from 'react'; +// @ts-ignore Remove once the test utils are typed +import { createRenderer, fireEvent } from '@mui/monorepo/test/utils'; +import { expect } from 'chai'; +import { DataGridPro, GridApi, useGridApiRef, GridColDef, gridClasses } from '@mui/x-data-grid-pro'; +import { getActiveCell, getCell, getColumnHeaderCell } from 'test/utils/helperFn'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +function fireClickEvent(cell: HTMLElement) { + fireEvent.mouseUp(cell); + fireEvent.click(cell); +} + +describe(' - Column Spanning', () => { + const { render } = createRenderer({ clock: 'fake' }); + + const baselineProps = { + rows: [ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ], + }; + + it('should not apply `colSpan` in pinned columns section if there is only one column there', function test() { + if (isJSDOM) { + // Need layouting + this.skip(); + } + + render( +
+ +
, + ); + + expect(getCell(0, 0).offsetWidth).to.equal(110); + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.not.throw(); + expect(() => getCell(0, 2)).to.not.throw(); + }); + + it('should apply `colSpan` inside pinned columns section', () => { + render( +
+ +
, + ); + + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.throw(/not found/); + expect(() => getCell(0, 2)).to.not.throw(); + }); + + /* eslint-disable material-ui/disallow-active-element-as-key-event-target */ + describe('key navigation', () => { + const columns: GridColDef[] = [ + { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]; + + it('should work after column reordering', () => { + let apiRef: React.MutableRefObject; + + const Test = () => { + apiRef = useGridApiRef(); + + return ( +
+ +
+ ); + }; + + render(); + + apiRef!.current.setColumnIndex('price', 1); + + fireClickEvent(getCell(1, 1)); + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + expect(getActiveCell()).to.equal('1-2'); + }); + }); + + it('should recalculate cells after column reordering', () => { + let apiRef: React.MutableRefObject; + + const Test = () => { + apiRef = useGridApiRef(); + + return ( +
+ (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]} + disableVirtualization={isJSDOM} + /> +
+ ); + }; + + render(); + + apiRef!.current.setColumnIndex('brand', 1); + + // Nike row + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.not.throw(); + expect(() => getCell(0, 2)).to.throw(/not found/); + expect(() => getCell(0, 3)).to.not.throw(); + + // Adidas row + expect(() => getCell(1, 0)).to.not.throw(); + expect(() => getCell(1, 1)).to.throw(/not found/); + expect(() => getCell(1, 2)).to.not.throw(); + expect(() => getCell(1, 3)).to.not.throw(); + + // Puma row + expect(() => getCell(2, 0)).to.not.throw(); + expect(() => getCell(2, 1)).to.not.throw(); + expect(() => getCell(2, 2)).to.not.throw(); + expect(() => getCell(2, 3)).to.throw(/not found/); + }); + + it('should work with column resizing', function test() { + if (isJSDOM) { + // Need layouting + this.skip(); + } + + const columns = [{ field: 'brand', colSpan: 2 }, { field: 'category' }, { field: 'price' }]; + + render( +
+ +
, + ); + + expect(getColumnHeaderCell(0).offsetWidth).to.equal(100); + expect(getColumnHeaderCell(1).offsetWidth).to.equal(100); + expect(getCell(0, 0).offsetWidth).to.equal(200); + + const separator = document.querySelector(`.${gridClasses['columnSeparator--resizable']}`); + fireEvent.mouseDown(separator, { clientX: 100 }); + fireEvent.mouseMove(separator, { clientX: 200, buttons: 1 }); + fireEvent.mouseUp(separator); + + expect(getColumnHeaderCell(0).offsetWidth).to.equal(200); + expect(getColumnHeaderCell(1).offsetWidth).to.equal(100); + expect(getCell(0, 0).offsetWidth).to.equal(300); + }); + + it('should apply `colSpan` correctly on GridApiRef setRows', () => { + const columns: GridColDef[] = [ + { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]; + + let apiRef: React.MutableRefObject; + + const Test = () => { + apiRef = useGridApiRef(); + + return ( +
+ +
+ ); + }; + + render(); + + apiRef!.current.setRows([ + { + id: 0, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 1, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 2, + brand: 'Reebok', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]); + + // Adidas row + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.not.throw(); + expect(() => getCell(0, 2)).to.throw(/not found/); + expect(() => getCell(0, 3)).to.not.throw(); + + // Nike row + expect(() => getCell(1, 0)).to.not.throw(); + expect(() => getCell(1, 1)).to.throw(/not found/); + expect(() => getCell(1, 2)).to.not.throw(); + expect(() => getCell(1, 3)).to.not.throw(); + + // Reebok row + expect(() => getCell(2, 0)).to.not.throw(); + expect(() => getCell(2, 1)).to.not.throw(); + expect(() => getCell(2, 2)).to.not.throw(); + expect(() => getCell(2, 3)).to.not.throw(); + }); +}); 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 a75c046c238fc..711de17217841 100644 --- a/packages/grid/x-data-grid-pro/src/utils/domUtils.ts +++ b/packages/grid/x-data-grid-pro/src/utils/domUtils.ts @@ -15,6 +15,9 @@ export function findGridCellElementsFromCol(col: HTMLElement): NodeListOf 1 and allocate `field` cell space + `.${gridClasses.cell}[data-field="${field}"], .${gridClasses.cell}[data-colspan-allocates-field-${field}="1"]`, + ); return cells; } diff --git a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx index 09f0e357fa9a3..2e585fe2aed1e 100644 --- a/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx +++ b/packages/grid/x-data-grid/src/DataGrid/useDataGridComponent.tsx @@ -47,6 +47,7 @@ import { useGridEvents } from '../hooks/features/events/useGridEvents'; import { useGridDimensions } from '../hooks/features/dimensions/useGridDimensions'; import { rowsMetaStateInitializer, useGridRowsMeta } from '../hooks/features/rows/useGridRowsMeta'; import { useGridStatePersistence } from '../hooks/features/statePersistence/useGridStatePersistence'; +import { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning'; export const useDataGridComponent = (props: DataGridProcessedProps) => { const apiRef = useGridInitialization(undefined, props); @@ -83,6 +84,7 @@ export const useDataGridComponent = (props: DataGridProcessedProps) => { useGridColumns(apiRef, props); useGridRows(apiRef, props); useGridParamsApi(apiRef); + useGridColumnSpanning(apiRef); const useGridEditing = props.experimentalFeatures?.newEditingApi ? useGridEditing_new diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 2ca9dadbe7da8..94e34734044c8 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -279,28 +279,39 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { ? 0 : -1; - cells.push( - - {content} - , + const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( + rowId, + indexRelativeToAllColumns, ); + + if (cellColSpanInfo && !cellColSpanInfo.collapsedByColSpan) { + const { colSpan, width, other: otherCellProps } = cellColSpanInfo.cellProps; + + cells.push( + + {content} + , + ); + } } const emptyCellWidth = containerWidth - columnsTotalWidth; 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 b11218a272e78..2ddb703c72985 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,5 +1,6 @@ import * as React from 'react'; import { useForkRef } from '@mui/material/utils'; +import { defaultMemoize } from 'reselect'; import { useGridApiContext } from '../../utils/useGridApiContext'; import { useGridSelector } from '../../utils/useGridSelector'; import { @@ -20,6 +21,9 @@ import { GridRenderContext } from '../../../models/params/gridScrollParams'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridEventListener, GridEvents } from '../../../models/events'; import { GridColumnHeaderItem } from '../../../components/columnHeaders/GridColumnHeaderItem'; +import { getFirstColumnIndexToRender } from '../columns/gridColumnsUtils'; +import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; +import { getRenderableIndexes } from '../virtualization/useGridVirtualScroller'; interface UseGridColumnHeadersProps { innerRef?: React.Ref; @@ -48,17 +52,38 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const [renderContext, setRenderContext] = React.useState(null); const prevRenderContext = React.useRef(renderContext); const prevScrollLeft = React.useRef(0); + const currentPage = useGridVisibleRows(apiRef, rootProps); React.useEffect(() => { apiRef.current.columnHeadersContainerElementRef!.current!.scrollLeft = 0; }, [apiRef]); + // memoize `getFirstColumnIndexToRender`, since it's called on scroll + const getFirstColumnIndexToRenderRef = React.useRef( + defaultMemoize(getFirstColumnIndexToRender, { + equalityCheck: (a, b) => + ['firstColumnIndex', 'minColumnIndex', 'columnBuffer'].every((key) => a[key] === b[key]), + }), + ); + const updateInnerPosition = React.useCallback( (nextRenderContext: GridRenderContext) => { - const firstColumnToRender = Math.max( - nextRenderContext!.firstColumnIndex - rootProps.columnBuffer, + const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ + firstIndex: nextRenderContext.firstRowIndex, + lastIndex: nextRenderContext.lastRowIndex, + minFirstIndex: 0, + maxLastIndex: currentPage.rows.length, + buffer: rootProps.rowBuffer, + }); + + const firstColumnToRender = getFirstColumnIndexToRenderRef.current({ + firstColumnIndex: nextRenderContext!.firstColumnIndex, minColumnIndex, - ); + columnBuffer: rootProps.columnBuffer, + firstRowToRender, + lastRowToRender, + apiRef, + }); const offset = firstColumnToRender > 0 @@ -67,7 +92,14 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { innerRef!.current!.style.transform = `translate3d(${-offset}px, 0px, 0px)`; }, - [columnPositions, minColumnIndex, rootProps.columnBuffer], + [ + columnPositions, + minColumnIndex, + rootProps.columnBuffer, + apiRef, + currentPage.rows.length, + rootProps.rowBuffer, + ], ); const handleScroll = React.useCallback>( @@ -143,10 +175,22 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { const columns: JSX.Element[] = []; - const firstColumnToRender = Math.max( - nextRenderContext!.firstColumnIndex! - rootProps.columnBuffer, - minFirstColumn, - ); + const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ + firstIndex: nextRenderContext.firstRowIndex, + lastIndex: nextRenderContext.lastRowIndex, + minFirstIndex: 0, + maxLastIndex: currentPage.rows.length, + buffer: rootProps.rowBuffer, + }); + + const firstColumnToRender = getFirstColumnIndexToRenderRef.current({ + firstColumnIndex: nextRenderContext!.firstColumnIndex, + minColumnIndex: minFirstColumn, + columnBuffer: rootProps.columnBuffer, + apiRef, + firstRowToRender, + lastRowToRender, + }); const lastColumnToRender = Math.min( nextRenderContext.lastColumnIndex! + rootProps.columnBuffer, diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts index 10ee0602a030b..f6a5fbb0cd3a4 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts @@ -18,6 +18,8 @@ import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridColDef, GridStateColDef } from '../../../models/colDef/gridColDef'; import { gridColumnsSelector, gridColumnVisibilityModelSelector } from './gridColumnsSelector'; import { clamp } from '../../../utils/utils'; +import { GridApiCommon } from '../../../models/api/gridApiCommon'; +import { gridVisibleSortedRowEntriesSelector } from '../filter/gridFilterSelector'; export const COLUMNS_DIMENSION_PROPERTIES = ['maxWidth', 'minWidth', 'width', 'flex'] as const; @@ -475,3 +477,54 @@ export const mergeColumnsState = ...state, columns: columnsState, }); + +export function getFirstNonSpannedColumnToRender({ + firstColumnToRender, + apiRef, + firstRowToRender, + lastRowToRender, +}: { + firstColumnToRender: number; + apiRef: React.MutableRefObject; + firstRowToRender: number; + lastRowToRender: number; +}) { + let firstNonSpannedColumnToRender = firstColumnToRender; + const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); + const renderedRows = visibleSortedRows.slice(firstRowToRender, lastRowToRender); + renderedRows.forEach((row) => { + const rowId = row.id; + const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, firstColumnToRender); + if (cellColSpanInfo && cellColSpanInfo.collapsedByColSpan) { + firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex; + } + }); + return firstNonSpannedColumnToRender; +} + +export function getFirstColumnIndexToRender({ + firstColumnIndex, + minColumnIndex, + columnBuffer, + firstRowToRender, + lastRowToRender, + apiRef, +}: { + firstColumnIndex: number; + minColumnIndex: number; + columnBuffer: number; + apiRef: React.MutableRefObject; + firstRowToRender: number; + lastRowToRender: number; +}) { + const initialFirstColumnToRender = Math.max(firstColumnIndex - columnBuffer, minColumnIndex); + + const firstColumnToRender = getFirstNonSpannedColumnToRender({ + firstColumnToRender: initialFirstColumnToRender, + apiRef, + firstRowToRender, + lastRowToRender, + }); + + return firstColumnToRender; +} diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts new file mode 100644 index 0000000000000..08cc060cb895d --- /dev/null +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -0,0 +1,142 @@ +import React from 'react'; +import { useGridApiMethod } from '../../utils/useGridApiMethod'; +import { GridColumnIndex, GridCellColSpanInfo } from '../../../models/gridColumnSpanning'; +import { GridRowId } from '../../../models/gridRows'; +import { GridColumnSpanning } from '../../../models/api/gridColumnSpanning'; +import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; +import { GridEvents } from '../../../models/events/gridEvents'; +import { GridCellParams } from '../../../models/params/gridCellParams'; +import { GridStateColDef } from '../../../models/colDef/gridColDef'; +import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; + +/** + * @requires useGridColumns (method, event) + * @requires useGridParamsApi (method) + */ +export const useGridColumnSpanning = (apiRef: React.MutableRefObject) => { + const lookup = React.useRef>>({}); + + const setCellColSpanInfo = React.useCallback( + (rowId: GridRowId, columnIndex: GridColumnIndex, cellColSpanInfo: GridCellColSpanInfo) => { + const sizes = lookup.current; + if (!sizes[rowId]) { + sizes[rowId] = {}; + } + + sizes[rowId][columnIndex] = cellColSpanInfo; + }, + [], + ); + + const getCellColSpanInfo = React.useCallback( + (rowId, columnIndex) => { + return lookup.current[rowId]?.[columnIndex]; + }, + [], + ); + + // Calculate `colSpan` for the cell. + const calculateCellColSpan = React.useCallback( + (params: { + columnIndex: number; + rowId: GridRowId; + cellParams: GridCellParams; + renderedColumns: GridStateColDef[]; + }) => { + const { columnIndex, rowId, cellParams, renderedColumns } = params; + const visibleColumns = apiRef.current.getVisibleColumns(); + const columnsLength = visibleColumns.length; + const column = visibleColumns[columnIndex]; + + let width = column.computedWidth; + + let colSpan = + typeof column.colSpan === 'function' ? column.colSpan(cellParams) : column.colSpan; + + if (typeof colSpan === 'undefined') { + colSpan = 1; + } + + // Attributes used by `useGridColumnResize` to update column width during resizing. + // This makes resizing smooth even for cells with colspan > 1. + const dataColSpanAttributes: Record = {}; + + if (colSpan > 1) { + for (let j = 1; j < colSpan; j += 1) { + const nextColumnIndex = columnIndex + j; + const nextColumn = visibleColumns[nextColumnIndex]; + // Use `renderedColumns` here to calculate colSpan for pinned and non-pinned columns in isolation. + if (renderedColumns.includes(nextColumn)) { + width += nextColumn.computedWidth; + + dataColSpanAttributes[ + /** + * `.toLowerCase()` is used to avoid React warning when using camelCase field name. + * querySelectorAll() still works when querying with camelCase field name. + */ + `data-colspan-allocates-field-${nextColumn.field.toLowerCase()}` + ] = '1'; + + setCellColSpanInfo(rowId, columnIndex + j, { + collapsedByColSpan: true, + rightVisibleCellIndex: Math.min(columnIndex + colSpan, columnsLength - 1), + leftVisibleCellIndex: columnIndex, + }); + } + } + dataColSpanAttributes['aria-colspan'] = String(colSpan); + } + + setCellColSpanInfo(rowId, columnIndex, { + collapsedByColSpan: false, + cellProps: { + colSpan, + width, + other: dataColSpanAttributes, + }, + }); + + return { + colSpan, + }; + }, + [apiRef, setCellColSpanInfo], + ); + // Calculate `colSpan` for each cell in the row + const calculateColSpan = React.useCallback( + ({ rowId, minFirstColumn, maxLastColumn }) => { + const visibleColumns = apiRef.current.getVisibleColumns(); + // `minFirstColumn` and `maxLastColumn` are used to make `colSpan` work with pinned columns. + // Cells should be spanned only within their column section. + const renderedColumns = visibleColumns.slice(minFirstColumn, maxLastColumn); + + for (let i = minFirstColumn; i < maxLastColumn; i += 1) { + const column = visibleColumns[i]; + const cellProps = calculateCellColSpan({ + columnIndex: i, + rowId, + renderedColumns, + cellParams: apiRef.current.getCellParams(rowId, column.field), + }); + if (cellProps.colSpan > 1) { + i += cellProps.colSpan - 1; + } + } + }, + [apiRef, calculateCellColSpan], + ); + + const columnSpanningApi: GridColumnSpanning = { + unstable_getCellColSpanInfo: getCellColSpanInfo, + unstable_calculateColSpan: calculateColSpan, + }; + + useGridApiMethod(apiRef, columnSpanningApi, 'GridColumnSpanningAPI'); + + const handleColumnReorderChange = React.useCallback(() => { + // `colSpan` needs to be recalculated after column reordering + lookup.current = {}; + }, []); + + useGridApiEventHandler(apiRef, GridEvents.columnOrderChange, handleColumnReorderChange); +}; diff --git a/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts b/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts index fe2e91b936387..65b05a9e0432c 100644 --- a/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts +++ b/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts @@ -21,6 +21,7 @@ import { gridClasses } from '../../../constants/gridClasses'; * @requires useGridDimensions (method) - can be after * @requires useGridFocus (method) - can be after * @requires useGridScroll (method) - can be after + * @requires useGridColumnSpanning (method) - can be after */ export const useGridKeyboardNavigation = ( apiRef: React.MutableRefObject, @@ -29,12 +30,26 @@ export const useGridKeyboardNavigation = ( const logger = useGridLogger(apiRef, 'useGridKeyboardNavigation'); const currentPage = useGridVisibleRows(apiRef, props); + /** + * @colIndex index of the column to focus + * @rowIndex index of the row to focus + * @closestColResolution Which closest column cell to focus when cell has `colSpan`. + */ const goToCell = React.useCallback( - (colIndex: number, rowIndex: number) => { + (colIndex: number, rowIndex: number, closestColResolution: 'left' | 'right' = 'left') => { + const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); + const rowId = visibleSortedRows[rowIndex]?.id; + const nextCellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, colIndex); + if (nextCellColSpanInfo && nextCellColSpanInfo.collapsedByColSpan) { + if (closestColResolution === 'left') { + colIndex = nextCellColSpanInfo.leftVisibleCellIndex; + } else if (closestColResolution === 'right') { + colIndex = nextCellColSpanInfo.rightVisibleCellIndex; + } + } logger.debug(`Navigating to cell row ${rowIndex}, col ${colIndex}`); apiRef.current.scrollToIndexes({ colIndex, rowIndex }); const field = apiRef.current.getVisibleColumns()[colIndex].field; - const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); const node = visibleSortedRows[rowIndex]; apiRef.current.setCellFocus(node.id, field); }, @@ -93,7 +108,7 @@ export const useGridKeyboardNavigation = ( case 'ArrowRight': { if (colIndexBefore < lastColIndex) { - goToCell(colIndexBefore + 1, rowIndexBefore); + goToCell(colIndexBefore + 1, rowIndexBefore, 'right'); } break; } @@ -108,9 +123,9 @@ export const useGridKeyboardNavigation = ( case 'Tab': { // "Tab" is only triggered by the row / cell editing feature if (event.shiftKey && colIndexBefore > firstColIndex) { - goToCell(colIndexBefore - 1, rowIndexBefore); + goToCell(colIndexBefore - 1, rowIndexBefore, 'left'); } else if (!event.shiftKey && colIndexBefore < lastColIndex) { - goToCell(colIndexBefore + 1, rowIndexBefore); + goToCell(colIndexBefore + 1, rowIndexBefore, 'right'); } break; } diff --git a/packages/grid/x-data-grid/src/hooks/features/scroll/useGridScroll.ts b/packages/grid/x-data-grid/src/hooks/features/scroll/useGridScroll.ts index 1844359f19fd3..675c9c01f1c7d 100644 --- a/packages/grid/x-data-grid/src/hooks/features/scroll/useGridScroll.ts +++ b/packages/grid/x-data-grid/src/hooks/features/scroll/useGridScroll.ts @@ -6,6 +6,7 @@ import { gridColumnPositionsSelector, gridVisibleColumnDefinitionsSelector, } from '../columns/gridColumnsSelector'; +import { useGridSelector } from '../../utils/useGridSelector'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { gridPageSelector, gridPageSizeSelector } from '../pagination/gridPaginationSelector'; import { gridRowCountSelector } from '../rows/gridRowsSelector'; @@ -14,6 +15,7 @@ import { GridScrollParams } from '../../../models/params/gridScrollParams'; import { GridScrollApi } from '../../../models/api/gridScrollApi'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridNativeEventListener } from '../../utils/useGridNativeEventListener'; +import { gridVisibleSortedRowEntriesSelector } from '../filter/gridFilterSelector'; // Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js // Similar to https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView @@ -40,6 +42,8 @@ function scrollIntoView(dimensions: { * @requires useGridColumns (state) - can be after, async only * @requires useGridRows (state) - can be after, async only * @requires useGridRowsMeta (state) - can be after, async only + * @requires useGridFilter (state) + * @requires useGridColumnSpanning (method) */ export const useGridScroll = ( apiRef: React.MutableRefObject, @@ -48,6 +52,7 @@ export const useGridScroll = ( const logger = useGridLogger(apiRef, 'useGridScroll'); const colRef = apiRef.current.columnHeadersElementRef!; const windowRef = apiRef.current.windowRef!; + const visibleSortedRows = useGridSelector(apiRef, gridVisibleSortedRowEntriesSelector); const scrollToIndexes = React.useCallback( (params: Partial) => { @@ -64,10 +69,27 @@ export const useGridScroll = ( if (params.colIndex != null) { const columnPositions = gridColumnPositionsSelector(apiRef); + let cellWidth: number | undefined; + + if (typeof params.rowIndex !== 'undefined') { + const rowId = visibleSortedRows[params.rowIndex]?.id; + const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( + rowId, + params.colIndex, + ); + if (cellColSpanInfo && !cellColSpanInfo.collapsedByColSpan) { + cellWidth = cellColSpanInfo.cellProps.width; + } + } + + if (typeof cellWidth === 'undefined') { + cellWidth = visibleColumns[params.colIndex].computedWidth; + } + scrollCoordinates.left = scrollIntoView({ clientHeight: windowRef.current!.clientWidth, scrollTop: windowRef.current!.scrollLeft, - offsetHeight: visibleColumns[params.colIndex].computedWidth, + offsetHeight: cellWidth, offsetTop: columnPositions[params.colIndex], }); } @@ -108,7 +130,7 @@ export const useGridScroll = ( return false; }, - [logger, apiRef, windowRef, props.pagination], + [logger, apiRef, windowRef, props.pagination, visibleSortedRows], ); const scroll = React.useCallback( diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index 22ac1c2259cd5..deaa01b67ed5c 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -19,6 +19,10 @@ import { GridRenderContext } from '../../../models'; import { selectedIdsLookupSelector } from '../selection/gridSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; +import { + getFirstNonSpannedColumnToRender, + getFirstColumnIndexToRender, +} from '../columns/gridColumnsUtils'; // Uses binary search to avoid looping through all possible positions export function getIndexFromScroll( @@ -42,6 +46,25 @@ export function getIndexFromScroll( : getIndexFromScroll(offset, positions, pivot + 1, sliceEnd); } +export const getRenderableIndexes = ({ + firstIndex, + lastIndex, + buffer, + minFirstIndex, + maxLastIndex, +}: { + firstIndex: number; + lastIndex: number; + buffer: number; + minFirstIndex: number; + maxLastIndex: number; +}) => { + return [ + clamp(firstIndex - buffer, minFirstIndex, maxLastIndex), + clamp(lastIndex + buffer, minFirstIndex, maxLastIndex), + ]; +}; + interface UseGridVirtualScrollerProps { ref: React.Ref; disableVirtualization?: boolean; @@ -141,41 +164,23 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { useGridApiEventHandler(apiRef, GridEvents.resize, handleResize); - const getRenderableIndexes = ({ - firstIndex, - lastIndex, - buffer, - minFirstIndex, - maxLastIndex, - }: { - firstIndex: number; - lastIndex: number; - buffer: number; - minFirstIndex: number; - maxLastIndex: number; - }) => { - return [ - clamp(firstIndex - buffer, minFirstIndex, maxLastIndex), - clamp(lastIndex + buffer, minFirstIndex, maxLastIndex), - ]; - }; - const updateRenderZonePosition = React.useCallback( (nextRenderContext: GridRenderContext) => { - const [firstRowToRender] = getRenderableIndexes({ + const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstRowIndex, lastIndex: nextRenderContext.lastRowIndex, minFirstIndex: 0, - maxLastIndex: currentPage.range?.lastRowIndex || 0, + maxLastIndex: currentPage.rows.length, buffer: rootProps.rowBuffer, }); - const [firstColumnToRender] = getRenderableIndexes({ - firstIndex: nextRenderContext.firstColumnIndex, - lastIndex: nextRenderContext.lastColumnIndex, - minFirstIndex: renderZoneMinColumnIndex, - maxLastIndex: renderZoneMaxColumnIndex, - buffer: rootProps.columnBuffer, + const firstColumnToRender = getFirstColumnIndexToRender({ + firstColumnIndex: nextRenderContext.firstColumnIndex, + minColumnIndex: renderZoneMinColumnIndex, + columnBuffer: rootProps.columnBuffer, + firstRowToRender, + lastRowToRender, + apiRef, }); const top = gridRowsMetaSelector(apiRef.current.state).positions[firstRowToRender]; @@ -188,9 +193,8 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { }, [ apiRef, - currentPage.range?.lastRowIndex, + currentPage.rows.length, onRenderZonePositioning, - renderZoneMaxColumnIndex, renderZoneMinColumnIndex, rootProps.columnBuffer, rootProps.rowBuffer, @@ -299,7 +303,14 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { buffer: rowBuffer, }); - const [firstColumnToRender, lastColumnToRender] = getRenderableIndexes({ + const renderedRows = currentPage.rows.slice(firstRowToRender, lastRowToRender); + + for (let i = 0; i < renderedRows.length; i += 1) { + const row = renderedRows[i]; + apiRef.current.unstable_calculateColSpan({ rowId: row.id, minFirstColumn, maxLastColumn }); + } + + const [initialFirstColumnToRender, lastColumnToRender] = getRenderableIndexes({ firstIndex: nextRenderContext.firstColumnIndex, lastIndex: nextRenderContext.lastColumnIndex, minFirstIndex: minFirstColumn, @@ -307,7 +318,13 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { buffer: columnBuffer, }); - const renderedRows = currentPage.rows.slice(firstRowToRender, lastRowToRender); + const firstColumnToRender = getFirstNonSpannedColumnToRender({ + firstColumnToRender: initialFirstColumnToRender, + apiRef, + firstRowToRender, + lastRowToRender, + }); + const renderedColumns = visibleColumns.slice(firstColumnToRender, lastColumnToRender); const rows: JSX.Element[] = []; diff --git a/packages/grid/x-data-grid/src/internals/index.ts b/packages/grid/x-data-grid/src/internals/index.ts index 025d2414d5645..3d84d722bb475 100644 --- a/packages/grid/x-data-grid/src/internals/index.ts +++ b/packages/grid/x-data-grid/src/internals/index.ts @@ -17,6 +17,7 @@ export { columnMenuStateInitializer, } from '../hooks/features/columnMenu/useGridColumnMenu'; export { useGridColumns, columnsStateInitializer } from '../hooks/features/columns/useGridColumns'; +export { useGridColumnSpanning } from '../hooks/features/columns/useGridColumnSpanning'; 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 f2babb4f96853..c8e43c607fe91 100644 --- a/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts +++ b/packages/grid/x-data-grid/src/models/api/gridApiCommon.ts @@ -20,6 +20,7 @@ import { GridStateApi } from './gridStateApi'; import { GridLoggerApi } from './gridLoggerApi'; import { GridScrollApi } from './gridScrollApi'; import { GridVirtualScrollerApi } from './gridVirtualScrollerApi'; +import { GridColumnSpanning } from './gridColumnSpanning'; import type { GridPreProcessingApi } from '../../hooks/core/preProcessing'; import type { GridStrategyProcessingApi } from '../../hooks/core/strategyProcessing'; import type { GridDimensionsApi } from '../../hooks/features/dimensions'; @@ -56,4 +57,5 @@ export interface GridApiCommon GridLocaleTextApi, GridClipboardApi, GridScrollApi, + GridColumnSpanning, GridStateApiUntyped {} diff --git a/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts b/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts new file mode 100644 index 0000000000000..8e07ee27b0446 --- /dev/null +++ b/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts @@ -0,0 +1,31 @@ +import { GridColumnIndex, GridCellColSpanInfo } from '../gridColumnSpanning'; +import { GridRowId } from '../gridRows'; +/** + * The Column Spanning API interface that is available in the grid `apiRef`. + */ +export interface GridColumnSpanning { + /** + * Returns cell colSpan info. + * @param {GridRowId} rowId The row id + * @param {number} columnIndex The column index (0-based) + * @returns {GridCellColSpanInfo|undefined} Cell colSpan info + * @ignore - do not document. + */ + unstable_getCellColSpanInfo: ( + rowId: GridRowId, + columnIndex: GridColumnIndex, + ) => GridCellColSpanInfo | undefined; + /** + * Calculate column spanning for each cell in the row + * @param {Object} options The options to apply on the calculation. + * @param {GridRowId} options.rowId The row id + * @param {number} options.minFirstColumn First visible column index + * @param {number} options.maxLastColumn Last visible column index + * @ignore - do not document. + */ + unstable_calculateColSpan: (options: { + rowId: GridRowId; + minFirstColumn: number; + maxLastColumn: 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 227eedf894abd..9598abf8b2c97 100644 --- a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts +++ b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts @@ -220,6 +220,11 @@ export interface GridColDef { * @default false */ disableExport?: boolean; + /** + * Number of columns a grid cell should span. + * @default 1 + */ + colSpan?: number | ((params: GridCellParams) => number | undefined); } export interface GridActionsColDef extends GridColDef { diff --git a/packages/grid/x-data-grid/src/models/gridColumnSpanning.ts b/packages/grid/x-data-grid/src/models/gridColumnSpanning.ts new file mode 100644 index 0000000000000..3fdbeb72b2c3f --- /dev/null +++ b/packages/grid/x-data-grid/src/models/gridColumnSpanning.ts @@ -0,0 +1,16 @@ +export type GridColumnIndex = number; + +export type GridCellColSpanInfo = + | { + collapsedByColSpan: true; + rightVisibleCellIndex: GridColumnIndex; + leftVisibleCellIndex: GridColumnIndex; + } + | { + collapsedByColSpan: false; + cellProps: { + colSpan: number; + width: number; + other?: Record; + }; + }; diff --git a/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx new file mode 100644 index 0000000000000..156be89dafbbc --- /dev/null +++ b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx @@ -0,0 +1,852 @@ +import * as React from 'react'; +// @ts-ignore Remove once the test utils are typed +import { createRenderer, fireEvent, waitFor, screen, within } from '@mui/monorepo/test/utils'; +import { expect } from 'chai'; +import { DataGrid, gridClasses, GridColDef } from '@mui/x-data-grid'; +import { getCell, getActiveCell, getColumnHeaderCell } from 'test/utils/helperFn'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +function fireClickEvent(cell: HTMLElement) { + fireEvent.mouseUp(cell); + fireEvent.click(cell); +} + +describe(' - Column Spanning', () => { + const { render, clock } = createRenderer({ clock: 'fake' }); + + const baselineProps = { + rows: [ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ], + }; + + it('should support `colSpan` number signature', () => { + render( +
+ +
, + ); + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.throw(/not found/); + expect(() => getCell(0, 2)).to.throw(/not found/); + expect(() => getCell(0, 3)).to.not.throw(); + }); + + it('should support `colSpan` function signature', () => { + render( +
+ (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]} + disableVirtualization={isJSDOM} + /> +
, + ); + // Nike + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.throw(/not found/); + expect(() => getCell(0, 2)).to.not.throw(); + expect(() => getCell(0, 3)).to.not.throw(); + + // Adidas + expect(() => getCell(1, 0)).to.not.throw(); + expect(() => getCell(1, 1)).to.not.throw(); + expect(() => getCell(1, 2)).to.throw(/not found/); + expect(() => getCell(1, 3)).to.not.throw(); + + // Puma + expect(() => getCell(2, 0)).to.not.throw(); + expect(() => getCell(2, 1)).to.not.throw(); + expect(() => getCell(2, 2)).to.not.throw(); + expect(() => getCell(2, 3)).to.throw(/not found/); + }); + + it('should treat `colSpan` 0 value as 1', () => { + render( +
+ 0 }, + { field: 'price' }, + ]} + rows={[{ id: 0, brand: 'Nike', category: 'Shoes', price: '$120' }]} + /> +
, + ); + // First Nike row + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.not.throw(); + expect(() => getCell(0, 2)).to.not.throw(); + }); + + /* eslint-disable material-ui/disallow-active-element-as-key-event-target */ + describe('key navigation', () => { + const columns: GridColDef[] = [ + { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]; + + it('should move to the cell right when pressing "ArrowRight"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 0)); + expect(getActiveCell()).to.equal('0-0'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + expect(getActiveCell()).to.equal('0-2'); + }); + + it('should move to the cell left when pressing "ArrowLeft"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 2)); + expect(getActiveCell()).to.equal('0-2'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + expect(getActiveCell()).to.equal('0-0'); + }); + + it('should move to the cell above when pressing "ArrowUp"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(1, 1)); + expect(getActiveCell()).to.equal('1-1'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowUp' }); + expect(getActiveCell()).to.equal('0-0'); + }); + + it('should move to the cell below when pressing "ArrowDown"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(1, 3)); + expect(getActiveCell()).to.equal('1-3'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + expect(getActiveCell()).to.equal('2-2'); + }); + + it('should move down by the amount of rows visible on screen when pressing "PageDown"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 3)); + expect(getActiveCell()).to.equal('0-3'); + + fireEvent.keyDown(document.activeElement!, { key: 'PageDown' }); + expect(getActiveCell()).to.equal('2-2'); + }); + + it('should move up by the amount of rows visible on screen when pressing "PageUp"', () => { + render( +
+ +
, + ); + + fireClickEvent(getCell(2, 1)); + expect(getActiveCell()).to.equal('2-1'); + + fireEvent.keyDown(document.activeElement!, { key: 'PageUp' }); + expect(getActiveCell()).to.equal('0-0'); + }); + + it('should move to the cell below when pressing "Enter" after editing', async () => { + const editableColumns = columns.map((column) => ({ ...column, editable: true })); + render( +
+ +
, + ); + + fireClickEvent(getCell(1, 3)); + expect(getActiveCell()).to.equal('1-3'); + + // start editing + fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + + // commit + fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + await waitFor(() => { + expect(getActiveCell()).to.equal('2-2'); + }); + }); + + it('should move to the cell on the right when pressing "Tab" after editing', async () => { + const editableColumns = columns.map((column) => ({ ...column, editable: true })); + render( +
+ +
, + ); + + fireClickEvent(getCell(1, 1)); + expect(getActiveCell()).to.equal('1-1'); + + // start editing + fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + + fireEvent.keyDown(document.activeElement!, { key: 'Tab' }); + await waitFor(() => { + expect(getActiveCell()).to.equal('1-3'); + }); + }); + + it('should move to the cell on the left when pressing "Shift+Tab" after editing', async () => { + const editableColumns = columns.map((column) => ({ ...column, editable: true })); + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 2)); + expect(getActiveCell()).to.equal('0-2'); + + // start editing + fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + + fireEvent.keyDown(document.activeElement!, { key: 'Tab', shiftKey: true }); + await waitFor(() => { + expect(getActiveCell()).to.equal('0-0'); + }); + }); + + it('should work with row virtualization', function test() { + if (isJSDOM) { + // needs virtualization + this.skip(); + } + + const rows = [ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + }, + { + id: 1, + brand: 'Nike', + category: 'Shoes', + price: '$120', + }, + { + id: 2, + brand: 'Nike', + category: 'Shoes', + price: '$120', + }, + + { + id: 3, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + }, + ]; + + const rowHeight = 52; + + render( +
+ (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'category' }, + { field: 'price' }, + ]} + rows={rows} + rowBuffer={1} + rowThreshold={1} + rowHeight={rowHeight} + /> +
, + ); + + fireClickEvent(getCell(1, 1)); + expect(getActiveCell()).to.equal('1-1'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + + const virtualScroller = document.querySelector(`.${gridClasses.virtualScroller}`)!; + // trigger virtualization + virtualScroller.dispatchEvent(new Event('scroll')); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + const activeCell = getActiveCell(); + expect(activeCell).to.equal('3-0'); + }); + + it('should work with column virtualization', function test() { + if (isJSDOM) { + this.skip(); // needs layout + } + + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 0)); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + document.querySelector(`.${gridClasses.virtualScroller}`)!.dispatchEvent(new Event('scroll')); + + expect(() => getCell(0, 3)).to.not.throw(); + // should not be rendered because of first column colSpan + expect(() => getCell(0, 2)).to.throw(/not found/); + }); + + it('should work with filtering', () => { + render( +
+ (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category' }, + { field: 'price' }, + { field: 'rating' }, + ]} + rows={[ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + { + id: 3, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 4, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 5, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]} + initialState={{ + filter: { + filterModel: { + items: [{ columnField: 'brand', operatorValue: 'equals', value: 'Nike' }], + }, + }, + }} + /> +
, + ); + + fireClickEvent(getCell(0, 0)); + expect(getActiveCell()).to.equal('0-0'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + expect(getActiveCell()).to.equal('1-0'); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + expect(getActiveCell()).to.equal('1-2'); + }); + + it('should scroll the whole cell into view when `colSpan` > 1', function test() { + if (isJSDOM) { + this.skip(); // needs layout + } + + render( +
+ +
, + ); + + fireClickEvent(getCell(0, 0)); + + const virtualScroller = document.querySelector( + `.${gridClasses.virtualScroller}`, + )! as HTMLElement; + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + virtualScroller.dispatchEvent(new Event('scroll')); + fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + virtualScroller.dispatchEvent(new Event('scroll')); + expect(getActiveCell()).to.equal('0-3'); + // should be scrolled to the end of the cell + expect(virtualScroller.scrollLeft).to.equal(5 * 100 - virtualScroller.offsetWidth); + + fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + virtualScroller.dispatchEvent(new Event('scroll')); + fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + virtualScroller.dispatchEvent(new Event('scroll')); + + expect(getActiveCell()).to.equal('0-0'); + expect(virtualScroller.scrollLeft).to.equal(0); + }); + }); + + it('should work with filtering', () => { + render( +
+ (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]} + rows={[ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + { + id: 3, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 4, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 5, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]} + initialState={{ + filter: { + filterModel: { + items: [{ columnField: 'brand', operatorValue: 'equals', value: 'Nike' }], + }, + }, + }} + disableVirtualization={isJSDOM} + /> +
, + ); + // First Nike row + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.throw(/not found/); + expect(() => getCell(0, 2)).to.not.throw(); + expect(() => getCell(0, 3)).to.not.throw(); + + // Second Nike row + expect(() => getCell(1, 0)).to.not.throw(); + expect(() => getCell(1, 1)).to.throw(/not found/); + expect(() => getCell(1, 2)).to.not.throw(); + expect(() => getCell(1, 3)).to.not.throw(); + }); + + it('should apply `colSpan` properly after hiding a column', () => { + render( +
+ (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]} + /> +
, + ); + + // hide `category` column + fireEvent.click(within(getColumnHeaderCell(1)).getByLabelText('Menu')); + fireEvent.click(screen.getByRole('menuitem', { name: 'Hide' })); + clock.runToLast(); + + // Nike row + expect(() => getCell(0, 0)).to.not.throw(); + expect(() => getCell(0, 1)).to.throw(/not found/); + expect(() => getCell(0, 2)).to.not.throw(); + + // Adidas row + expect(() => getCell(1, 0)).to.not.throw(); + expect(() => getCell(1, 1)).to.not.throw(); + expect(() => getCell(1, 2)).to.not.throw(); + + // Puma row + expect(() => getCell(2, 0)).to.not.throw(); + expect(() => getCell(2, 1)).to.not.throw(); + expect(() => getCell(2, 2)).to.throw(/not found/); + }); + + it('should add `aria-colspan` attribute when `colSpan` > 1', () => { + render( +
+ +
, + ); + + expect(getCell(0, 0).getAttribute('aria-colspan')).to.equal('2'); + expect(getCell(0, 2).getAttribute('aria-colspan')).to.equal(null); + }); + + it('should work with pagination', () => { + const rows = [ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + { + id: 3, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 4, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 5, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]; + + const columns: GridColDef[] = [ + { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, + { field: 'category', colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1) }, + { field: 'price', colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1) }, + { field: 'rating' }, + ]; + + const pageSize = 2; + + function checkRows(pageNumber: number, rowsList: Array<'Nike' | 'Adidas' | 'Puma'>) { + rowsList.forEach((rowName, index) => { + const rowIndex = pageNumber * pageSize + index; + if (rowName === 'Nike') { + expect(() => getCell(rowIndex, 0)).to.not.throw(); + expect(() => getCell(rowIndex, 1)).to.throw(/not found/); + expect(() => getCell(rowIndex, 2)).to.not.throw(); + expect(() => getCell(rowIndex, 3)).to.not.throw(); + } else if (rowName === 'Adidas') { + expect(() => getCell(rowIndex, 0)).to.not.throw(); + expect(() => getCell(rowIndex, 1)).to.not.throw(); + expect(() => getCell(rowIndex, 2)).to.throw(/not found/); + expect(() => getCell(rowIndex, 3)).to.not.throw(); + } else if (rowName === 'Puma') { + expect(() => getCell(rowIndex, 0)).to.not.throw(); + expect(() => getCell(rowIndex, 1)).to.not.throw(); + expect(() => getCell(rowIndex, 2)).to.not.throw(); + expect(() => getCell(rowIndex, 3)).to.throw(/not found/); + } + }); + } + + render( +
+ +
, + ); + + checkRows(0, ['Nike', 'Adidas']); + + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + checkRows(1, ['Puma', 'Nike']); + + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + checkRows(2, ['Adidas', 'Puma']); + }); + + it('should work with column virtualization', function test() { + if (isJSDOM) { + // Need layouting for column virtualization + this.skip(); + } + + render( +
+ (value === '1-0' ? 3 : 1) }, + { field: 'col1', width: 100 }, + { field: 'col2', width: 100 }, + { field: 'col3', width: 100 }, + { field: 'col4', width: 100 }, + { field: 'col5', width: 100 }, + ]} + rows={[ + { id: 0, col0: '0-0', col1: '0-1', col2: '0-2', col3: '0-3', col4: '0-4', col5: '0-5' }, + { id: 1, col0: '1-0', col1: '1-1', col2: '1-2', col3: '1-3', col4: '1-4', col5: '1-5' }, + ]} + columnBuffer={1} + columnThreshold={1} + /> +
, + ); + + expect(getCell(0, 4).offsetLeft).to.equal( + getCell(1, 4).offsetLeft, + 'last cells in both rows should be aligned', + ); + + expect(getColumnHeaderCell(4).offsetLeft).to.equal( + getCell(1, 4).offsetLeft, + 'last cell and column header cell should be aligned', + ); + + const virtualScroller = document.querySelector(`.${gridClasses.virtualScroller}`)!; + // scroll to the very end + virtualScroller.scrollLeft = 1000; + virtualScroller.dispatchEvent(new Event('scroll')); + + expect(getCell(0, 5).offsetLeft).to.equal( + getCell(1, 5).offsetLeft, + 'last cells in both rows should be aligned after scroll', + ); + + expect(getColumnHeaderCell(5).offsetLeft).to.equal( + getCell(1, 5).offsetLeft, + 'last cell and column header cell should be aligned after scroll', + ); + }); + + it('should work with both column and row virtualization', function test() { + if (isJSDOM) { + // Need layouting for column virtualization + this.skip(); + } + + const rowHeight = 50; + + render( +
+ (value === '1-0' ? 3 : 1) }, + { field: 'col1', width: 100 }, + { field: 'col2', width: 100 }, + { field: 'col3', width: 100 }, + { field: 'col4', width: 100 }, + { field: 'col5', width: 100 }, + ]} + rows={[ + { id: 0, col0: '0-0', col1: '0-1', col2: '0-2', col3: '0-3', col4: '0-4', col5: '0-5' }, + { id: 1, col0: '1-0', col1: '1-1', col2: '1-2', col3: '1-3', col4: '1-4', col5: '1-5' }, + { id: 2, col0: '2-0', col1: '2-1', col2: '2-2', col3: '2-3', col4: '2-4', col5: '2-5' }, + { id: 3, col0: '3-0', col1: '3-1', col2: '3-2', col3: '3-3', col4: '3-4', col5: '3-5' }, + { id: 4, col0: '4-0', col1: '4-1', col2: '4-2', col3: '4-3', col4: '4-4', col5: '4-5' }, + { id: 5, col0: '5-0', col1: '5-1', col2: '5-2', col3: '5-3', col4: '5-4', col5: '5-5' }, + { id: 6, col0: '6-0', col1: '6-1', col2: '6-2', col3: '6-3', col4: '6-4', col5: '6-5' }, + ]} + columnBuffer={1} + columnThreshold={1} + rowBuffer={1} + rowThreshold={1} + rowHeight={rowHeight} + /> +
, + ); + + expect(getCell(2, 4).offsetLeft).to.equal( + getCell(1, 4).offsetLeft, + 'last cells in both rows should be aligned', + ); + + expect(getColumnHeaderCell(4).offsetLeft).to.equal( + getCell(1, 4).offsetLeft, + 'last cell and column header cell should be aligned', + ); + + const virtualScroller = document.querySelector(`.${gridClasses.virtualScroller}`)!; + // scroll to the very end + virtualScroller.scrollLeft = 1000; + // hide first row to trigger row virtualization + virtualScroller.scrollTop = rowHeight + 10; + virtualScroller.dispatchEvent(new Event('scroll')); + + expect(getCell(2, 5).offsetLeft).to.equal( + getCell(1, 5).offsetLeft, + 'last cells in both rows should be aligned after scroll', + ); + + expect(getColumnHeaderCell(5).offsetLeft).to.equal( + getCell(1, 5).offsetLeft, + 'last cell and column header cell should be aligned after scroll', + ); + }); +}); diff --git a/packages/storybook/src/stories/grid-columns.stories.tsx b/packages/storybook/src/stories/grid-columns.stories.tsx index 4c70f2bdf332a..35942e7e5e651 100644 --- a/packages/storybook/src/stories/grid-columns.stories.tsx +++ b/packages/storybook/src/stories/grid-columns.stories.tsx @@ -448,3 +448,174 @@ export function PinnedColumnWithCheckboxSelectionSnap() { ); } + +export function ColumnSpanning() { + const columns = [ + { + field: 'brand', + colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1), + editable: true, + }, + { + field: 'category', + colSpan: ({ row }) => (row.brand === 'Adidas' ? 2 : 1), + editable: true, + }, + { + field: 'price', + colSpan: ({ row }) => (row.brand === 'Puma' ? 2 : 1), + editable: true, + }, + { field: 'rating', editable: true }, + ]; + + const rows = [ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]; + + return ( +
+ +
+ ); +} + +export function ColumnSpanningWithRowVirtualization() { + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 12, + }); + + const columns = data.columns.map((column) => ({ + ...column, + colSpan: + column.field === 'commodity' + ? ({ row }) => { + if (row.commodity === 'Rapeseed') { + return 3; + } + return 1; + } + : 1, + })); + + return ( +
+ +
+ ); +} + +export function ColumnSpanningWithColumnVirtualization() { + return ( +
+ +
+ ); +} + +export function ColumnSpanningWithFiltering() { + return ( +
+ (row.brand === 'Nike' ? 2 : 1), + }, + { field: 'category' }, + { field: 'price' }, + { field: 'rating' }, + ]} + rows={[ + { + id: 0, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 1, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 2, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + { + id: 3, + brand: 'Nike', + category: 'Shoes', + price: '$120', + rating: '4.5', + }, + { + id: 4, + brand: 'Adidas', + category: 'Shoes', + price: '$100', + rating: '4.5', + }, + { + id: 5, + brand: 'Puma', + category: 'Shoes', + price: '$90', + rating: '4.5', + }, + ]} + initialState={{ + filter: { + filterModel: { + items: [{ columnField: 'brand', operatorValue: 'equals', value: 'Nike' }], + }, + }, + }} + /> +
+ ); +} From bfda43724d08cb97060fcd2f567414ff2bb138eb Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Fri, 18 Mar 2022 15:22:10 +0100 Subject: [PATCH 02/33] pass aria-colspan to Cell directly --- packages/grid/x-data-grid/src/components/GridRow.tsx | 1 + .../src/hooks/features/columns/useGridColumnSpanning.ts | 1 - .../grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 94e34734044c8..1bbb3c53e12bc 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -305,6 +305,7 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { tabIndex={tabIndex} className={clsx(classNames)} colSpan={colSpan} + aria-colspan={String(colSpan)} {...otherCellProps} {...rootProps.componentsProps?.cell} > diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index 08cc060cb895d..be577ca634230 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -84,7 +84,6 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject - Column Spanning', () => { ); expect(getCell(0, 0).getAttribute('aria-colspan')).to.equal('2'); - expect(getCell(0, 2).getAttribute('aria-colspan')).to.equal(null); + expect(getCell(0, 2).getAttribute('aria-colspan')).to.equal('1'); }); it('should work with pagination', () => { From 1fc6eed667b208af5f0a4a1f02a51209bdfdd662 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Fri, 18 Mar 2022 18:34:33 +0100 Subject: [PATCH 03/33] perf: get rid of expensive Array.include --- .../features/columns/useGridColumnSpanning.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index be577ca634230..89bd1c1e7950c 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -6,7 +6,6 @@ import { GridColumnSpanning } from '../../../models/api/gridColumnSpanning'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridEvents } from '../../../models/events/gridEvents'; import { GridCellParams } from '../../../models/params/gridCellParams'; -import { GridStateColDef } from '../../../models/colDef/gridColDef'; import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; /** @@ -41,9 +40,10 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject { - const { columnIndex, rowId, cellParams, renderedColumns } = params; + const { columnIndex, rowId, cellParams, minFirstColumnIndex, maxLastColumnIndex } = params; const visibleColumns = apiRef.current.getVisibleColumns(); const columnsLength = visibleColumns.length; const column = visibleColumns[columnIndex]; @@ -64,9 +64,9 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject 1) { for (let j = 1; j < colSpan; j += 1) { const nextColumnIndex = columnIndex + j; - const nextColumn = visibleColumns[nextColumnIndex]; - // Use `renderedColumns` here to calculate colSpan for pinned and non-pinned columns in isolation. - if (renderedColumns.includes(nextColumn)) { + // Cells should be spanned only within their column section (left-pinned, right-pinned and unpinned). + if (nextColumnIndex >= minFirstColumnIndex && nextColumnIndex < maxLastColumnIndex) { + const nextColumn = visibleColumns[nextColumnIndex]; width += nextColumn.computedWidth; dataColSpanAttributes[ @@ -105,16 +105,14 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject( ({ rowId, minFirstColumn, maxLastColumn }) => { const visibleColumns = apiRef.current.getVisibleColumns(); - // `minFirstColumn` and `maxLastColumn` are used to make `colSpan` work with pinned columns. - // Cells should be spanned only within their column section. - const renderedColumns = visibleColumns.slice(minFirstColumn, maxLastColumn); for (let i = minFirstColumn; i < maxLastColumn; i += 1) { const column = visibleColumns[i]; const cellProps = calculateCellColSpan({ columnIndex: i, rowId, - renderedColumns, + minFirstColumnIndex: minFirstColumn, + maxLastColumnIndex: maxLastColumn, cellParams: apiRef.current.getCellParams(rowId, column.field), }); if (cellProps.colSpan > 1) { From 44d3ae2d1ea0dc5f5dfed9180e7a671071dfad01 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Fri, 18 Mar 2022 18:34:52 +0100 Subject: [PATCH 04/33] rename `collapsedByColSpan` --- packages/grid/x-data-grid/src/components/GridRow.tsx | 2 +- .../src/hooks/features/columns/gridColumnsUtils.ts | 2 +- .../src/hooks/features/columns/useGridColumnSpanning.ts | 4 ++-- .../src/hooks/features/keyboard/useGridKeyboardNavigation.ts | 2 +- .../x-data-grid/src/hooks/features/scroll/useGridScroll.ts | 2 +- packages/grid/x-data-grid/src/models/gridColumnSpanning.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 1bbb3c53e12bc..978acd175050f 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -284,7 +284,7 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { indexRelativeToAllColumns, ); - if (cellColSpanInfo && !cellColSpanInfo.collapsedByColSpan) { + if (cellColSpanInfo && !cellColSpanInfo.spannedByColSpan) { const { colSpan, width, other: otherCellProps } = cellColSpanInfo.cellProps; cells.push( diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts index f6a5fbb0cd3a4..257d45f16838a 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts @@ -495,7 +495,7 @@ export function getFirstNonSpannedColumnToRender({ renderedRows.forEach((row) => { const rowId = row.id; const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, firstColumnToRender); - if (cellColSpanInfo && cellColSpanInfo.collapsedByColSpan) { + if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan) { firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex; } }); diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index 89bd1c1e7950c..c597fccb26b4a 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -78,7 +78,7 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject Date: Fri, 18 Mar 2022 18:51:10 +0100 Subject: [PATCH 05/33] improve readability --- .../features/columns/useGridColumnSpanning.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index c597fccb26b4a..df03e28be6588 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -48,20 +48,29 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject 1. - const dataColSpanAttributes: Record = {}; + if (colSpan === 1) { + setCellColSpanInfo(rowId, columnIndex, { + spannedByColSpan: false, + cellProps: { + colSpan: 1, + width: column.computedWidth, + other: {}, + }, + }); + } else { + // Attributes used by `useGridColumnResize` to update column width during resizing. + // This makes resizing smooth even for cells with colspan > 1. + const dataColSpanAttributes: Record = {}; + + let width = column.computedWidth; - if (colSpan > 1) { for (let j = 1; j < colSpan; j += 1) { const nextColumnIndex = columnIndex + j; // Cells should be spanned only within their column section (left-pinned, right-pinned and unpinned). @@ -83,24 +92,25 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject( ({ rowId, minFirstColumn, maxLastColumn }) => { From 78bb8a42a9f3c7846e7eac4c5d2675cbfa0ed57a Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Fri, 18 Mar 2022 18:55:48 +0100 Subject: [PATCH 06/33] perf: get rid of expensive Array.slice --- .../features/columns/gridColumnsUtils.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts index 257d45f16838a..0813244b7e09e 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts @@ -491,14 +491,20 @@ export function getFirstNonSpannedColumnToRender({ }) { let firstNonSpannedColumnToRender = firstColumnToRender; const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); - const renderedRows = visibleSortedRows.slice(firstRowToRender, lastRowToRender); - renderedRows.forEach((row) => { - const rowId = row.id; - const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, firstColumnToRender); - if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan) { - firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex; + for (let i = firstRowToRender; i < lastRowToRender; i += 1) { + const row = visibleSortedRows[i]; + if (row) { + const rowId = visibleSortedRows[i].id; + const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( + rowId, + firstColumnToRender, + ); + if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan) { + firstNonSpannedColumnToRender = cellColSpanInfo.leftVisibleCellIndex; + } } - }); + } + return firstNonSpannedColumnToRender; } From bfd1ed15a213f69e2dc1ab5826b4b87c9b80b16d Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Fri, 18 Mar 2022 19:08:53 +0100 Subject: [PATCH 07/33] rename column spanning api type --- .../features/columns/useGridColumnSpanning.ts | 17 ++++++++--------- .../x-data-grid/src/models/api/gridApiCommon.ts | 4 ++-- .../src/models/api/gridColumnSpanning.ts | 4 +++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index df03e28be6588..69e3ef96d943c 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -2,7 +2,7 @@ import React from 'react'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { GridColumnIndex, GridCellColSpanInfo } from '../../../models/gridColumnSpanning'; import { GridRowId } from '../../../models/gridRows'; -import { GridColumnSpanning } from '../../../models/api/gridColumnSpanning'; +import { GridColumnSpanningApi } from '../../../models/api/gridColumnSpanning'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridEvents } from '../../../models/events/gridEvents'; import { GridCellParams } from '../../../models/params/gridCellParams'; @@ -27,12 +27,11 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject( - (rowId, columnIndex) => { - return lookup.current[rowId]?.[columnIndex]; - }, - [], - ); + const getCellColSpanInfo = React.useCallback< + GridColumnSpanningApi['unstable_getCellColSpanInfo'] + >((rowId, columnIndex) => { + return lookup.current[rowId]?.[columnIndex]; + }, []); // Calculate `colSpan` for the cell. const calculateCellColSpan = React.useCallback( @@ -112,7 +111,7 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject( + const calculateColSpan = React.useCallback( ({ rowId, minFirstColumn, maxLastColumn }) => { const visibleColumns = apiRef.current.getVisibleColumns(); @@ -133,7 +132,7 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject Date: Fri, 18 Mar 2022 19:48:24 +0100 Subject: [PATCH 08/33] get rid of Array.slice --- .../features/virtualization/useGridVirtualScroller.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index deaa01b67ed5c..121eaefcea87e 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -15,7 +15,7 @@ import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GridEventListener, GridEvents } from '../../../models/events'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { clamp } from '../../../utils/utils'; -import { GridRenderContext } from '../../../models'; +import { GridRenderContext, GridRowEntry } from '../../../models'; import { selectedIdsLookupSelector } from '../selection/gridSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; @@ -303,10 +303,12 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { buffer: rowBuffer, }); - const renderedRows = currentPage.rows.slice(firstRowToRender, lastRowToRender); + const renderedRows: GridRowEntry[] = []; + + for (let i = firstRowToRender; i < lastRowToRender; i += 1) { + const row = currentPage.rows[i]; + renderedRows.push(row); - for (let i = 0; i < renderedRows.length; i += 1) { - const row = renderedRows[i]; apiRef.current.unstable_calculateColSpan({ rowId: row.id, minFirstColumn, maxLastColumn }); } From 0f5551a7f1843607dae6f12284cf75503219feb2 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 21 Mar 2022 12:40:38 +0100 Subject: [PATCH 09/33] fix jsdoc --- .../features/keyboard/useGridKeyboardNavigation.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts b/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts index 445dd3d172db8..628bf9aedb713 100644 --- a/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts +++ b/packages/grid/x-data-grid/src/hooks/features/keyboard/useGridKeyboardNavigation.ts @@ -31,19 +31,19 @@ export const useGridKeyboardNavigation = ( const currentPage = useGridVisibleRows(apiRef, props); /** - * @colIndex index of the column to focus - * @rowIndex index of the row to focus - * @closestColResolution Which closest column cell to focus when cell has `colSpan`. + * @param {number} colIndex Index of the column to focus + * @param {number} rowIndex index of the row to focus + * @param {string} closestColumnToUse Which closest column cell to use when the cell is spanned by `colSpan`. */ const goToCell = React.useCallback( - (colIndex: number, rowIndex: number, closestColResolution: 'left' | 'right' = 'left') => { + (colIndex: number, rowIndex: number, closestColumnToUse: 'left' | 'right' = 'left') => { const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); const rowId = visibleSortedRows[rowIndex]?.id; const nextCellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo(rowId, colIndex); if (nextCellColSpanInfo && nextCellColSpanInfo.spannedByColSpan) { - if (closestColResolution === 'left') { + if (closestColumnToUse === 'left') { colIndex = nextCellColSpanInfo.leftVisibleCellIndex; - } else if (closestColResolution === 'right') { + } else if (closestColumnToUse === 'right') { colIndex = nextCellColSpanInfo.rightVisibleCellIndex; } } From 7f9806d96a28e5591468347b4a65f07c93063601 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 21 Mar 2022 13:31:35 +0100 Subject: [PATCH 10/33] do not use data-attributes to query cells for column resizing --- .../columnResize/useGridColumnResize.tsx | 9 ++--- .../x-data-grid-pro/src/utils/domUtils.ts | 37 ++++++++++++++++--- .../x-data-grid/src/components/GridRow.tsx | 3 +- .../features/columns/useGridColumnSpanning.ts | 14 ------- .../src/models/gridColumnSpanning.ts | 1 - 5 files changed, 36 insertions(+), 28 deletions(-) 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 3830c1b18a6a2..3ac14115a3430 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 @@ -120,7 +120,7 @@ export const useGridColumnResize = ( const colDefRef = React.useRef(); const colElementRef = React.useRef(); - const colCellElementsRef = React.useRef>(); + const colCellElementsRef = React.useRef(); // To improve accessibility, the separator has padding on both sides. // Clicking inside the padding area should be treated as a click in the separator. @@ -241,7 +241,8 @@ export const useGridColumnResize = ( colCellElementsRef.current = findGridCellElementsFromCol( colElementRef.current, - ) as NodeListOf; + apiRef.current, + ); const doc = ownerDocument(apiRef.current.rootElementRef!.current as HTMLElement); doc.body.style.cursor = 'col-resize'; @@ -345,9 +346,7 @@ export const useGridColumnResize = ( apiRef.current.columnHeadersElementRef?.current!, colDef.field, ) as HTMLDivElement; - colCellElementsRef.current = findGridCellElementsFromCol( - colElementRef.current, - ) as NodeListOf; + colCellElementsRef.current = findGridCellElementsFromCol(colElementRef.current, apiRef.current); separatorSide.current = getSeparatorSide(event.target); 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 711de17217841..ef33671ad9dd6 100644 --- a/packages/grid/x-data-grid-pro/src/utils/domUtils.ts +++ b/packages/grid/x-data-grid-pro/src/utils/domUtils.ts @@ -1,5 +1,6 @@ import { gridClasses } from '@mui/x-data-grid'; import { findParentElementFromClassName } from '@mui/x-data-grid/internals'; +import { GridApiPro } from '../models/gridApiPro'; export function getFieldFromHeaderElem(colCellEl: Element): string { return colCellEl.getAttribute('data-field')!; @@ -9,15 +10,39 @@ export function findHeaderElementFromField(elem: Element, field: string): Elemen return elem.querySelector(`[data-field="${field}"]`); } -export function findGridCellElementsFromCol(col: HTMLElement): NodeListOf | null { - const field = col.getAttribute('data-field'); +export function findGridCellElementsFromCol(col: HTMLElement, api: GridApiPro) { const root = findParentElementFromClassName(col, 'MuiDataGrid-root'); if (!root) { throw new Error('MUI: The root element is not found.'); } - const cells = root.querySelectorAll( - // Include cells that have colspan > 1 and allocate `field` cell space - `.${gridClasses.cell}[data-field="${field}"], .${gridClasses.cell}[data-colspan-allocates-field-${field}="1"]`, - ); + + const ariaColIndex = col.getAttribute('aria-colindex'); + if (!ariaColIndex) { + return []; + } + + const colIndex = Number(ariaColIndex) - 1; + const cells: Element[] = []; + + const renderedRowElements = root.querySelectorAll(`.${gridClasses.row}`); + + renderedRowElements.forEach((rowElement) => { + const rowId = rowElement.getAttribute('data-id'); + if (!rowId) { + return; + } + + let columnIndex = colIndex; + + const cellColSpanInfo = api.unstable_getCellColSpanInfo(rowId, colIndex); + if (cellColSpanInfo && cellColSpanInfo.spannedByColSpan) { + columnIndex = cellColSpanInfo.leftVisibleCellIndex; + } + const cell = rowElement.querySelector(`[data-colindex="${columnIndex}"]`); + if (cell) { + cells.push(cell); + } + }); + return cells; } diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 978acd175050f..8e2ec9f1d59a3 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -285,7 +285,7 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { ); if (cellColSpanInfo && !cellColSpanInfo.spannedByColSpan) { - const { colSpan, width, other: otherCellProps } = cellColSpanInfo.cellProps; + const { colSpan, width } = cellColSpanInfo.cellProps; cells.push( & GridRowProps) { className={clsx(classNames)} colSpan={colSpan} aria-colspan={String(colSpan)} - {...otherCellProps} {...rootProps.componentsProps?.cell} > {content} diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index 69e3ef96d943c..0a7fcc62aeebb 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -60,14 +60,9 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject 1. - const dataColSpanAttributes: Record = {}; - let width = column.computedWidth; for (let j = 1; j < colSpan; j += 1) { @@ -77,14 +72,6 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject; }; }; From aa01afb4a30dccc94ae4addc77a6f791cb12ddd1 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Tue, 22 Mar 2022 19:09:32 +0100 Subject: [PATCH 11/33] return early if colSpan === 1 --- .../features/columns/useGridColumnSpanning.ts | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index 0a7fcc62aeebb..dd845f338b2f5 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -47,14 +47,10 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject= minFirstColumnIndex && nextColumnIndex < maxLastColumnIndex) { - const nextColumn = visibleColumns[nextColumnIndex]; - width += nextColumn.computedWidth; - - setCellColSpanInfo(rowId, columnIndex + j, { - spannedByColSpan: true, - rightVisibleCellIndex: Math.min(columnIndex + colSpan, columnsLength - 1), - leftVisibleCellIndex: columnIndex, - }); - } - - setCellColSpanInfo(rowId, columnIndex, { - spannedByColSpan: false, - cellProps: { - colSpan, - width, - }, + return { colSpan: 1 }; + } + + let width = column.computedWidth; + + for (let j = 1; j < colSpan; j += 1) { + const nextColumnIndex = columnIndex + j; + // Cells should be spanned only within their column section (left-pinned, right-pinned and unpinned). + if (nextColumnIndex >= minFirstColumnIndex && nextColumnIndex < maxLastColumnIndex) { + const nextColumn = visibleColumns[nextColumnIndex]; + width += nextColumn.computedWidth; + + setCellColSpanInfo(rowId, columnIndex + j, { + spannedByColSpan: true, + rightVisibleCellIndex: Math.min(columnIndex + colSpan, columnsLength - 1), + leftVisibleCellIndex: columnIndex, }); } + + setCellColSpanInfo(rowId, columnIndex, { + spannedByColSpan: false, + cellProps: { colSpan, width }, + }); } - return { - colSpan, - }; + return { colSpan }; }, [apiRef, setCellColSpanInfo], ); From cb66205868289798713550ef33e58cfa9989b89f Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Tue, 22 Mar 2022 19:20:23 +0100 Subject: [PATCH 12/33] do not pass `cellParams` to `calculateCellColSpan` --- .../features/columns/useGridColumnSpanning.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts index dd845f338b2f5..3f8589904e650 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/useGridColumnSpanning.ts @@ -5,7 +5,6 @@ import { GridRowId } from '../../../models/gridRows'; import { GridColumnSpanningApi } from '../../../models/api/gridColumnSpanning'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridEvents } from '../../../models/events/gridEvents'; -import { GridCellParams } from '../../../models/params/gridCellParams'; import { GridApiCommunity } from '../../../models/api/gridApiCommunity'; /** @@ -38,17 +37,18 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject { - const { columnIndex, rowId, cellParams, minFirstColumnIndex, maxLastColumnIndex } = params; + const { columnIndex, rowId, minFirstColumnIndex, maxLastColumnIndex } = params; const visibleColumns = apiRef.current.getVisibleColumns(); const columnsLength = visibleColumns.length; const column = visibleColumns[columnIndex]; const colSpan = - typeof column.colSpan === 'function' ? column.colSpan(cellParams) : column.colSpan; + typeof column.colSpan === 'function' + ? column.colSpan(apiRef.current.getCellParams(rowId, column.field)) + : column.colSpan; if (!colSpan || colSpan === 1) { setCellColSpanInfo(rowId, columnIndex, { @@ -91,23 +91,19 @@ export const useGridColumnSpanning = (apiRef: React.MutableRefObject( ({ rowId, minFirstColumn, maxLastColumn }) => { - const visibleColumns = apiRef.current.getVisibleColumns(); - for (let i = minFirstColumn; i < maxLastColumn; i += 1) { - const column = visibleColumns[i]; const cellProps = calculateCellColSpan({ columnIndex: i, rowId, minFirstColumnIndex: minFirstColumn, maxLastColumnIndex: maxLastColumn, - cellParams: apiRef.current.getCellParams(rowId, column.field), }); if (cellProps.colSpan > 1) { i += cellProps.colSpan - 1; } } }, - [apiRef, calculateCellColSpan], + [calculateCellColSpan], ); const columnSpanningApi: GridColumnSpanningApi = { From 7b3b63c0ead83ea57999bc17e9ad658a4f7a6384 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Tue, 22 Mar 2022 19:36:07 +0100 Subject: [PATCH 13/33] do not pass `colSpan` attribute to GridCell div element --- .../src/hooks/features/columnResize/useGridColumnResize.tsx | 2 +- packages/grid/x-data-grid/src/components/GridRow.tsx | 1 - packages/grid/x-data-grid/src/components/cell/GridCell.tsx | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) 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 3ac14115a3430..461b1fda8917d 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 @@ -149,7 +149,7 @@ export const useGridColumnResize = ( const div = element as HTMLDivElement; let finalWidth: `${number}px`; - if (div.getAttribute('colspan') === '1') { + if (div.getAttribute('aria-colspan') === '1') { finalWidth = `${newWidth}px`; } else { // Cell with colspan > 1 cannot be just updated width new width. diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 8e2ec9f1d59a3..b99db4bb9e4f8 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -305,7 +305,6 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { tabIndex={tabIndex} className={clsx(classNames)} colSpan={colSpan} - aria-colspan={String(colSpan)} {...rootProps.componentsProps?.cell} > {content} diff --git a/packages/grid/x-data-grid/src/components/cell/GridCell.tsx b/packages/grid/x-data-grid/src/components/cell/GridCell.tsx index 710c9577ff4b6..338df6953a35a 100644 --- a/packages/grid/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/grid/x-data-grid/src/components/cell/GridCell.tsx @@ -35,6 +35,7 @@ export interface GridCellProps { cellMode?: GridCellMode; children: React.ReactNode; tabIndex: 0 | -1; + colSpan?: number; onClick?: React.MouseEventHandler; onDoubleClick?: React.MouseEventHandler; onMouseDown?: React.MouseEventHandler; @@ -100,6 +101,7 @@ function GridCell(props: GridCellProps) { showRightBorder, extendRowFullWidth, row, + colSpan, onClick, onDoubleClick, onMouseDown, @@ -219,6 +221,7 @@ function GridCell(props: GridCellProps) { data-field={field} data-colindex={colIndex} aria-colindex={colIndex + 1} + aria-colspan={colSpan} style={style} tabIndex={cellMode === 'view' || !isEditable ? tabIndex : -1} onClick={publish(GridEvents.cellClick, onClick)} @@ -250,6 +253,7 @@ GridCell.propTypes = { children: PropTypes.node, className: PropTypes.string, colIndex: PropTypes.number.isRequired, + colSpan: PropTypes.number, field: PropTypes.string.isRequired, formattedValue: PropTypes.oneOfType([ PropTypes.instanceOf(Date), From 9bc8d89ba438c3ed52b7c2e1b085944efc458b0e Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Tue, 22 Mar 2022 19:41:19 +0100 Subject: [PATCH 14/33] update docs --- docs/data/data-grid/columns/columns.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index 339b10bb94b40..abb8a353aaa92 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -511,8 +511,8 @@ To pin the checkbox column added when using `checkboxSelection`, add `GRID_CHECK ## Column spanning -Each cell takes up the width of one column. -You can modify this default behavior with column spanning. +By default, each cell takes up the width of one column. +You can modify this behavior with column spanning. It allows cells to span multiple columns. This is very close to the "column spanning" in an HTML `
`. @@ -529,12 +529,12 @@ interface GridColDef { } ``` -> When using `colSpan`, sorting and filtering might not work as expected. +> ⚠ When using `colSpan`, sorting and filtering might not work as expected. > Make sure to disable [sorting](/components/data-grid/sorting/#disable-the-sorting) and [filtering](/components/data-grid/filtering/#disable-the-filters) for the column(s) that are affected by `colSpan`. -> While [column reorder](/components/data-grid/columns/#column-reorder) works with `colSpan`, disabling it can be useful to avoid confusing grid layout. +> ⚠ While [column reorder](/components/data-grid/columns/#column-reorder) works with `colSpan`, disabling it can be useful to avoid confusing grid layout. ### Number signature From b03d63ec1364cce4d5479fe424bdb1f621afb98a Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Wed, 23 Mar 2022 17:18:23 +0100 Subject: [PATCH 15/33] use getFirstNonSpannedColumnToRender directly --- .../virtualization/useGridVirtualScroller.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index 121eaefcea87e..90f231696e72d 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -19,10 +19,7 @@ import { GridRenderContext, GridRowEntry } from '../../../models'; import { selectedIdsLookupSelector } from '../selection/gridSelectionSelector'; import { gridRowsMetaSelector } from '../rows/gridRowsMetaSelector'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; -import { - getFirstNonSpannedColumnToRender, - getFirstColumnIndexToRender, -} from '../columns/gridColumnsUtils'; +import { getFirstNonSpannedColumnToRender } from '../columns/gridColumnsUtils'; // Uses binary search to avoid looping through all possible positions export function getIndexFromScroll( @@ -174,13 +171,19 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { buffer: rootProps.rowBuffer, }); - const firstColumnToRender = getFirstColumnIndexToRender({ - firstColumnIndex: nextRenderContext.firstColumnIndex, - minColumnIndex: renderZoneMinColumnIndex, - columnBuffer: rootProps.columnBuffer, + const [initialFirstColumnToRender] = getRenderableIndexes({ + firstIndex: nextRenderContext.firstColumnIndex, + lastIndex: nextRenderContext.lastColumnIndex, + minFirstIndex: renderZoneMinColumnIndex, + maxLastIndex: renderZoneMaxColumnIndex, + buffer: rootProps.columnBuffer, + }); + + const firstColumnToRender = getFirstNonSpannedColumnToRender({ + firstColumnToRender: initialFirstColumnToRender, + apiRef, firstRowToRender, lastRowToRender, - apiRef, }); const top = gridRowsMetaSelector(apiRef.current.state).positions[firstRowToRender]; @@ -196,6 +199,7 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { currentPage.rows.length, onRenderZonePositioning, renderZoneMinColumnIndex, + renderZoneMaxColumnIndex, rootProps.columnBuffer, rootProps.rowBuffer, ], From aa8a2c81c85c298edb387fd6fa2ac98e8ce1d944 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Wed, 23 Mar 2022 19:35:00 +0100 Subject: [PATCH 16/33] improve ColumnSpanningDerived demo --- .../columns/ColumnSpanningDerived.tsx | 186 +++++++++++++----- 1 file changed, 132 insertions(+), 54 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx index 1d354b6c58d95..657f5a5c740fb 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx @@ -8,38 +8,94 @@ const slotTimesLookup = { 2: '11:00 - 12:00', 3: '12:00 - 13:00', 4: '13:00 - 14:00', + 5: '14:00 - 15:00', + 6: '15:00 - 16:00', + 7: '16:00 - 17:00', }; -const rows = [ +type Subject = + | 'Maths' + | 'English' + | 'Lab' + | 'Chemistry' + | 'Physics' + | 'Music' + | 'Dance'; + +const rows: Array<{ id: number; day: string; slots: Array }> = [ { id: 1, day: 'Monday', - slots: ['Maths', 'English', 'English', 'Lab', 'Lab'], + slots: ['Maths', 'English', 'English', 'Lab', 'Break', 'Lab', 'Music', 'Music'], }, { id: 2, day: 'Tuesday', - slots: ['Chemistry', 'Chemistry', 'Chemistry', 'Physics', 'Maths'], + slots: [ + 'Chemistry', + 'Chemistry', + 'Chemistry', + 'Physics', + 'Break', + 'Maths', + 'Lab', + 'Dance', + ], }, { id: 3, day: 'Wednesday', - slots: ['Physics', 'Chemistry', 'Physics', 'Maths', 'Maths'], + slots: [ + 'Physics', + 'English', + 'Maths', + 'Maths', + 'Break', + 'Chemistry', + 'Chemistry', + ], + }, + { + id: 4, + day: 'Thursday', + slots: [ + 'Music', + 'Music', + 'Chemistry', + 'Chemistry', + 'Break', + 'Chemistry', + 'English', + 'English', + ], + }, + { + id: 5, + day: 'Friday', + slots: ['Maths', 'Dance', 'Dance', 'Physics', 'Break', 'English'], }, ]; -function slotColSpan({ row, field, value }: GridCellParams) { - const index = Number(field); - let colSpan = 1; - let nextIndex = index + 1; - let nextValue = row.slots[nextIndex]; - while (nextValue === value) { - colSpan += 1; - nextIndex += 1; - nextValue = row.slots[nextIndex]; - } - return colSpan; -} +const slotColumnCommonFields: Partial = { + sortable: false, + filterable: false, + minWidth: 140, + cellClassName: (params: GridCellParams) => params.value, + colSpan: ({ row, field, value }: GridCellParams) => { + const index = Number(field); + let colSpan = 1; + for (let i = index + 1; i < row.slots.length; i += 1) { + const nextValue = row.slots[i]; + console.log('value', value, 'nextValue', nextValue); + if (nextValue === value) { + colSpan += 1; + } else { + break; + } + } + return colSpan; + }, +}; const columns: GridColDef[] = [ { @@ -49,68 +105,90 @@ const columns: GridColDef[] = [ { field: '0', headerName: slotTimesLookup[0], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[0]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[0], + ...slotColumnCommonFields, }, { field: '1', headerName: slotTimesLookup[1], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[1]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[1], + ...slotColumnCommonFields, }, { field: '2', headerName: slotTimesLookup[2], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[2]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[2], + ...slotColumnCommonFields, }, { field: '3', headerName: slotTimesLookup[3], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[3]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[3], + ...slotColumnCommonFields, }, { field: '4', headerName: slotTimesLookup[4], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[4]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[4], + ...slotColumnCommonFields, + }, + { + field: '5', + headerName: slotTimesLookup[5], + valueGetter: ({ row }) => row.slots[5], + ...slotColumnCommonFields, + }, + { + field: '6', + headerName: slotTimesLookup[6], + valueGetter: ({ row }) => row.slots[6], + ...slotColumnCommonFields, + }, + { + field: '7', + headerName: slotTimesLookup[7], + valueGetter: ({ row }) => row.slots[7], + ...slotColumnCommonFields, }, ]; +console.log('columns', columns); + +const rootStyles = { + width: '100%', + '& .Maths': { + backgroundColor: 'rgba(157, 255, 118, 0.49)', + }, + '& .English': { + backgroundColor: 'rgba(255, 255, 10, 0.49)', + }, + '& .Lab': { + backgroundColor: 'rgba(150, 150, 150, 0.49)', + }, + '& .Chemistry': { + backgroundColor: 'rgba(255, 150, 150, 0.49)', + }, + '& .Physics': { + backgroundColor: 'rgba(10, 150, 255, 0.49)', + }, + '& .Music': { + backgroundColor: 'rgba(224, 183, 60, 0.55)', + }, + '& .Dance': { + backgroundColor: 'rgba(200, 150, 255, 0.49)', + }, +}; + export default function ColumnSpanningDerived() { return ( - + Date: Wed, 23 Mar 2022 22:07:01 +0100 Subject: [PATCH 17/33] update js demo --- .../columns/ColumnSpanningDerived.js | 175 ++++++++++++------ .../columns/ColumnSpanningDerived.tsx.preview | 6 +- 2 files changed, 126 insertions(+), 55 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.js b/docs/data/data-grid/columns/ColumnSpanningDerived.js index 9021c16793d46..f6c643f87b3a4 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.js +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.js @@ -8,38 +8,85 @@ const slotTimesLookup = { 2: '11:00 - 12:00', 3: '12:00 - 13:00', 4: '13:00 - 14:00', + 5: '14:00 - 15:00', + 6: '15:00 - 16:00', + 7: '16:00 - 17:00', }; const rows = [ { id: 1, day: 'Monday', - slots: ['Maths', 'English', 'English', 'Lab', 'Lab'], + slots: ['Maths', 'English', 'English', 'Lab', 'Break', 'Lab', 'Music', 'Music'], }, { id: 2, day: 'Tuesday', - slots: ['Chemistry', 'Chemistry', 'Chemistry', 'Physics', 'Maths'], + slots: [ + 'Chemistry', + 'Chemistry', + 'Chemistry', + 'Physics', + 'Break', + 'Maths', + 'Lab', + 'Dance', + ], }, { id: 3, day: 'Wednesday', - slots: ['Physics', 'Chemistry', 'Physics', 'Maths', 'Maths'], + slots: [ + 'Physics', + 'English', + 'Maths', + 'Maths', + 'Break', + 'Chemistry', + 'Chemistry', + ], + }, + { + id: 4, + day: 'Thursday', + slots: [ + 'Music', + 'Music', + 'Chemistry', + 'Chemistry', + 'Break', + 'Chemistry', + 'English', + 'English', + ], + }, + { + id: 5, + day: 'Friday', + slots: ['Maths', 'Dance', 'Dance', 'Physics', 'Break', 'English'], }, ]; -function slotColSpan({ row, field, value }) { - const index = Number(field); - let colSpan = 1; - let nextIndex = index + 1; - let nextValue = row.slots[nextIndex]; - while (nextValue === value) { - colSpan += 1; - nextIndex += 1; - nextValue = row.slots[nextIndex]; - } - return colSpan; -} +const slotColumnCommonFields = { + sortable: false, + filterable: false, + minWidth: 140, + cellClassName: (params) => params.value, + colSpan: ({ row, field, value }) => { + const index = Number(field); + let colSpan = 1; + for (let i = index + 1; i < row.slots.length; i += 1) { + const nextValue = row.slots[i]; + console.log('value', value, 'nextValue', nextValue); + if (nextValue === value) { + colSpan += 1; + } else { + break; + } + } + return colSpan; + }, +}; const columns = [ { @@ -49,68 +96,90 @@ const columns = [ { field: '0', headerName: slotTimesLookup[0], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[0]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[0], + ...slotColumnCommonFields, }, { field: '1', headerName: slotTimesLookup[1], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[1]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[1], + ...slotColumnCommonFields, }, { field: '2', headerName: slotTimesLookup[2], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[2]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[2], + ...slotColumnCommonFields, }, { field: '3', headerName: slotTimesLookup[3], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[3]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[3], + ...slotColumnCommonFields, }, { field: '4', headerName: slotTimesLookup[4], - sortable: false, - filterable: false, - valueGetter: ({ row }) => { - return row.slots[4]; - }, - colSpan: slotColSpan, - flex: 1, + valueGetter: ({ row }) => row.slots[4], + ...slotColumnCommonFields, + }, + { + field: '5', + headerName: slotTimesLookup[5], + valueGetter: ({ row }) => row.slots[5], + ...slotColumnCommonFields, + }, + { + field: '6', + headerName: slotTimesLookup[6], + valueGetter: ({ row }) => row.slots[6], + ...slotColumnCommonFields, + }, + { + field: '7', + headerName: slotTimesLookup[7], + valueGetter: ({ row }) => row.slots[7], + ...slotColumnCommonFields, }, ]; +console.log('columns', columns); + +const rootStyles = { + width: '100%', + '& .Maths': { + backgroundColor: 'rgba(157, 255, 118, 0.49)', + }, + '& .English': { + backgroundColor: 'rgba(255, 255, 10, 0.49)', + }, + '& .Lab': { + backgroundColor: 'rgba(150, 150, 150, 0.49)', + }, + '& .Chemistry': { + backgroundColor: 'rgba(255, 150, 150, 0.49)', + }, + '& .Physics': { + backgroundColor: 'rgba(10, 150, 255, 0.49)', + }, + '& .Music': { + backgroundColor: 'rgba(224, 183, 60, 0.55)', + }, + '& .Dance': { + backgroundColor: 'rgba(200, 150, 255, 0.49)', + }, +}; + export default function ColumnSpanningDerived() { return ( - + Date: Thu, 24 Mar 2022 07:01:12 +0100 Subject: [PATCH 18/33] update warning on colSpan in combination with other features --- docs/data/data-grid/columns/columns.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index abb8a353aaa92..31103a9eec945 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -529,12 +529,12 @@ interface GridColDef { } ``` -> ⚠ When using `colSpan`, sorting and filtering might not work as expected. -> Make sure to disable [sorting](/components/data-grid/sorting/#disable-the-sorting) and [filtering](/components/data-grid/filtering/#disable-the-filters) for the column(s) that are affected by `colSpan`. - - - -> ⚠ While [column reorder](/components/data-grid/columns/#column-reorder) works with `colSpan`, disabling it can be useful to avoid confusing grid layout. +> ⚠ When using `colSpan`, some other features may be pointless or may not work as expected (depending on the data model). To avoid confusing grid layout, consider disabling the following features for the column(s) that are affected by `colSpan`: +> +> - [sorting](/components/data-grid/sorting/#disable-the-sorting) +> - [filtering](/components/data-grid/filtering/#disable-the-filters) +> - [column reorder](/components/data-grid/columns/#column-reorder) +> - [hiding columns](/components/data-grid/columns/#column-visibility) ### Number signature From b142999abf13199536c509ab3ce924d455ea415c Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 4 Apr 2022 16:40:00 +0200 Subject: [PATCH 19/33] build api docs --- docs/pages/api-docs/data-grid/grid-col-def.md | 1 + docs/pages/x/api/data-grid/grid-col-def.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/pages/api-docs/data-grid/grid-col-def.md b/docs/pages/api-docs/data-grid/grid-col-def.md index 6d51eda8058ac..a0008e606e017 100644 --- a/docs/pages/api-docs/data-grid/grid-col-def.md +++ b/docs/pages/api-docs/data-grid/grid-col-def.md @@ -16,6 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | +| colSpan? | number \| ((params: GridCellParams) => number \| undefined) | 1 | Number of columns a grid cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | diff --git a/docs/pages/x/api/data-grid/grid-col-def.md b/docs/pages/x/api/data-grid/grid-col-def.md index 6d51eda8058ac..a0008e606e017 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.md +++ b/docs/pages/x/api/data-grid/grid-col-def.md @@ -16,6 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | +| colSpan? | number \| ((params: GridCellParams) => number \| undefined) | 1 | Number of columns a grid cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | From a53c5e29059d990df5599ae88d9f115d0cbc6565 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 4 Apr 2022 16:40:31 +0200 Subject: [PATCH 20/33] make break slot empty --- .../columns/ColumnSpanningDerived.js | 18 +++++------------ .../columns/ColumnSpanningDerived.tsx | 20 ++++++------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.js b/docs/data/data-grid/columns/ColumnSpanningDerived.js index f6c643f87b3a4..bef6db0903bfb 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.js +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.js @@ -17,7 +17,7 @@ const rows = [ { id: 1, day: 'Monday', - slots: ['Maths', 'English', 'English', 'Lab', 'Break', 'Lab', 'Music', 'Music'], + slots: ['Maths', 'English', 'English', 'Lab', '', 'Lab', 'Music', 'Music'], }, { id: 2, @@ -27,7 +27,7 @@ const rows = [ 'Chemistry', 'Chemistry', 'Physics', - 'Break', + '', 'Maths', 'Lab', 'Dance', @@ -36,15 +36,7 @@ const rows = [ { id: 3, day: 'Wednesday', - slots: [ - 'Physics', - 'English', - 'Maths', - 'Maths', - 'Break', - 'Chemistry', - 'Chemistry', - ], + slots: ['Physics', 'English', 'Maths', 'Maths', '', 'Chemistry', 'Chemistry'], }, { id: 4, @@ -54,7 +46,7 @@ const rows = [ 'Music', 'Chemistry', 'Chemistry', - 'Break', + '', 'Chemistry', 'English', 'English', @@ -63,7 +55,7 @@ const rows = [ { id: 5, day: 'Friday', - slots: ['Maths', 'Dance', 'Dance', 'Physics', 'Break', 'English'], + slots: ['Maths', 'Dance', 'Dance', 'Physics', '', 'English'], }, ]; diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx index 657f5a5c740fb..45e5200967616 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx @@ -22,11 +22,11 @@ type Subject = | 'Music' | 'Dance'; -const rows: Array<{ id: number; day: string; slots: Array }> = [ +const rows: Array<{ id: number; day: string; slots: Array }> = [ { id: 1, day: 'Monday', - slots: ['Maths', 'English', 'English', 'Lab', 'Break', 'Lab', 'Music', 'Music'], + slots: ['Maths', 'English', 'English', 'Lab', '', 'Lab', 'Music', 'Music'], }, { id: 2, @@ -36,7 +36,7 @@ const rows: Array<{ id: number; day: string; slots: Array }> 'Chemistry', 'Chemistry', 'Physics', - 'Break', + '', 'Maths', 'Lab', 'Dance', @@ -45,15 +45,7 @@ const rows: Array<{ id: number; day: string; slots: Array }> { id: 3, day: 'Wednesday', - slots: [ - 'Physics', - 'English', - 'Maths', - 'Maths', - 'Break', - 'Chemistry', - 'Chemistry', - ], + slots: ['Physics', 'English', 'Maths', 'Maths', '', 'Chemistry', 'Chemistry'], }, { id: 4, @@ -63,7 +55,7 @@ const rows: Array<{ id: number; day: string; slots: Array }> 'Music', 'Chemistry', 'Chemistry', - 'Break', + '', 'Chemistry', 'English', 'English', @@ -72,7 +64,7 @@ const rows: Array<{ id: number; day: string; slots: Array }> { id: 5, day: 'Friday', - slots: ['Maths', 'Dance', 'Dance', 'Physics', 'Break', 'English'], + slots: ['Maths', 'Dance', 'Dance', 'Physics', '', 'English'], }, ]; From d25aedae985714eedc833305594108b649864fdc Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 4 Apr 2022 17:06:08 +0200 Subject: [PATCH 21/33] remove console logs --- docs/data/data-grid/columns/ColumnSpanningDerived.js | 3 --- docs/data/data-grid/columns/ColumnSpanningDerived.tsx | 3 --- 2 files changed, 6 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.js b/docs/data/data-grid/columns/ColumnSpanningDerived.js index bef6db0903bfb..f26ec2ff0059d 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.js +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.js @@ -69,7 +69,6 @@ const slotColumnCommonFields = { let colSpan = 1; for (let i = index + 1; i < row.slots.length; i += 1) { const nextValue = row.slots[i]; - console.log('value', value, 'nextValue', nextValue); if (nextValue === value) { colSpan += 1; } else { @@ -135,8 +134,6 @@ const columns = [ }, ]; -console.log('columns', columns); - const rootStyles = { width: '100%', '& .Maths': { diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx index 45e5200967616..8a6fd5276445b 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx @@ -78,7 +78,6 @@ const slotColumnCommonFields: Partial = { let colSpan = 1; for (let i = index + 1; i < row.slots.length; i += 1) { const nextValue = row.slots[i]; - console.log('value', value, 'nextValue', nextValue); if (nextValue === value) { colSpan += 1; } else { @@ -144,8 +143,6 @@ const columns: GridColDef[] = [ }, ]; -console.log('columns', columns); - const rootStyles = { width: '100%', '& .Maths': { From 43e2020bd5ca767f2d774695140156008a1b1e73 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 4 Apr 2022 17:20:27 +0200 Subject: [PATCH 22/33] add failing unit test --- .../tests/columnSpanning.DataGrid.test.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx index 5952ce65b6f26..4390c17bef358 100644 --- a/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx +++ b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx @@ -849,4 +849,73 @@ describe(' - Column Spanning', () => { 'last cell and column header cell should be aligned after scroll', ); }); + + it('should work with pagination and column virtualization', function test() { + if (isJSDOM) { + // Need layouting for column virtualization + this.skip(); + } + + const rowHeight = 50; + + render( +
+ (value === '4-0' ? 3 : 1) }, + { field: 'col1', width: 100 }, + { field: 'col2', width: 100 }, + { field: 'col3', width: 100 }, + { field: 'col4', width: 100 }, + { field: 'col5', width: 100 }, + ]} + rows={[ + { id: 0, col0: '0-0', col1: '0-1', col2: '0-2', col3: '0-3', col4: '0-4', col5: '0-5' }, + { id: 1, col0: '1-0', col1: '1-1', col2: '1-2', col3: '1-3', col4: '1-4', col5: '1-5' }, + { id: 2, col0: '2-0', col1: '2-1', col2: '2-2', col3: '2-3', col4: '2-4', col5: '2-5' }, + { id: 3, col0: '3-0', col1: '3-1', col2: '3-2', col3: '3-3', col4: '3-4', col5: '3-5' }, + { id: 4, col0: '4-0', col1: '4-1', col2: '4-2', col3: '4-3', col4: '4-4', col5: '4-5' }, + { id: 5, col0: '5-0', col1: '5-1', col2: '5-2', col3: '5-3', col4: '5-4', col5: '5-5' }, + ]} + columnBuffer={1} + columnThreshold={1} + rowBuffer={1} + rowThreshold={1} + rowHeight={rowHeight} + /> +
, + ); + + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + + expect(getCell(5, 4).offsetLeft).to.equal( + getCell(4, 4).offsetLeft, + 'last cells in both rows should be aligned', + ); + + expect(getColumnHeaderCell(4).offsetLeft).to.equal( + getCell(4, 4).offsetLeft, + 'last cell and column header cell should be aligned', + ); + + const virtualScroller = document.querySelector(`.${gridClasses.virtualScroller}`)!; + // scroll to the very end + virtualScroller.scrollLeft = 1000; + // hide first row to trigger row virtualization + virtualScroller.scrollTop = rowHeight + 10; + virtualScroller.dispatchEvent(new Event('scroll')); + + expect(getCell(5, 5).offsetLeft).to.equal( + getCell(4, 5).offsetLeft, + 'last cells in both rows should be aligned after scroll', + ); + + expect(getColumnHeaderCell(5).offsetLeft).to.equal( + getCell(4, 5).offsetLeft, + 'last cell and column header cell should be aligned after scroll', + ); + }); }); From ac54ad315c8e78ceba6f51541a6f4be173756c07 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 4 Apr 2022 18:20:55 +0200 Subject: [PATCH 23/33] fix failing test --- .../features/columnHeaders/useGridColumnHeaders.tsx | 4 +++- .../src/hooks/features/columns/gridColumnsUtils.ts | 12 ++++++++---- .../virtualization/useGridVirtualScroller.tsx | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) 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 b8eeab42875ee..b583b37341985 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 @@ -83,6 +83,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { firstRowToRender, lastRowToRender, apiRef, + visibleRows: currentPage.rows, }); const offset = @@ -97,7 +98,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { minColumnIndex, rootProps.columnBuffer, apiRef, - currentPage.rows.length, + currentPage.rows, rootProps.rowBuffer, ], ); @@ -190,6 +191,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { apiRef, firstRowToRender, lastRowToRender, + visibleRows: currentPage.rows, }); const lastColumnToRender = Math.min( diff --git a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts index b7c45c94496c1..2bfd11b1215f4 100644 --- a/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts +++ b/packages/grid/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts @@ -15,7 +15,7 @@ import { GridColDef, GridStateColDef } from '../../../models/colDef/gridColDef'; import { gridColumnsSelector, gridColumnVisibilityModelSelector } from './gridColumnsSelector'; import { clamp } from '../../../utils/utils'; import { GridApiCommon } from '../../../models/api/gridApiCommon'; -import { gridVisibleSortedRowEntriesSelector } from '../filter/gridFilterSelector'; +import { GridRowEntry } from '../../../models/gridRows'; export const COLUMNS_DIMENSION_PROPERTIES = ['maxWidth', 'minWidth', 'width', 'flex'] as const; @@ -501,18 +501,19 @@ export function getFirstNonSpannedColumnToRender({ apiRef, firstRowToRender, lastRowToRender, + visibleRows, }: { firstColumnToRender: number; apiRef: React.MutableRefObject; firstRowToRender: number; lastRowToRender: number; + visibleRows: GridRowEntry[]; }) { let firstNonSpannedColumnToRender = firstColumnToRender; - const visibleSortedRows = gridVisibleSortedRowEntriesSelector(apiRef); for (let i = firstRowToRender; i < lastRowToRender; i += 1) { - const row = visibleSortedRows[i]; + const row = visibleRows[i]; if (row) { - const rowId = visibleSortedRows[i].id; + const rowId = visibleRows[i].id; const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( rowId, firstColumnToRender, @@ -533,6 +534,7 @@ export function getFirstColumnIndexToRender({ firstRowToRender, lastRowToRender, apiRef, + visibleRows, }: { firstColumnIndex: number; minColumnIndex: number; @@ -540,6 +542,7 @@ export function getFirstColumnIndexToRender({ apiRef: React.MutableRefObject; firstRowToRender: number; lastRowToRender: number; + visibleRows: GridRowEntry[]; }) { const initialFirstColumnToRender = Math.max(firstColumnIndex - columnBuffer, minColumnIndex); @@ -548,6 +551,7 @@ export function getFirstColumnIndexToRender({ apiRef, firstRowToRender, lastRowToRender, + visibleRows, }); return firstColumnToRender; diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index 90f231696e72d..3ab38d59d35de 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -184,6 +184,7 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { apiRef, firstRowToRender, lastRowToRender, + visibleRows: currentPage.rows, }); const top = gridRowsMetaSelector(apiRef.current.state).positions[firstRowToRender]; @@ -196,7 +197,7 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { }, [ apiRef, - currentPage.rows.length, + currentPage.rows, onRenderZonePositioning, renderZoneMinColumnIndex, renderZoneMaxColumnIndex, @@ -329,6 +330,7 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { apiRef, firstRowToRender, lastRowToRender, + visibleRows: currentPage.rows, }); const renderedColumns = visibleColumns.slice(firstColumnToRender, lastColumnToRender); From d3d0aff4aa304d0b6fb9b739831c222a597f4092 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 12:30:30 +0200 Subject: [PATCH 24/33] use generics in colSpan --- packages/grid/x-data-grid/src/models/colDef/gridColDef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47a97426ecdbd..1c3d44be1b83e 100644 --- a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts +++ b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts @@ -229,7 +229,7 @@ export interface GridColDef { * Number of columns a grid cell should span. * @default 1 */ - colSpan?: number | ((params: GridCellParams) => number | undefined); + colSpan?: number | ((params: GridCellParams) => number | undefined); } export interface GridActionsColDef extends GridColDef { From af9f0e39ea1f31c0d3001cd42c8808bfa2f8ae38 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 13:00:33 +0200 Subject: [PATCH 25/33] fix ts errors --- .../columns/ColumnSpanningFunction.tsx | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.tsx b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx index 01ec8f5aab43f..b6c3d3bc90e13 100644 --- a/docs/data/data-grid/columns/ColumnSpanningFunction.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.tsx @@ -1,17 +1,44 @@ import * as React from 'react'; import Box from '@mui/material/Box'; -import { DataGridPro } from '@mui/x-data-grid-pro'; +import { DataGridPro, DataGridProProps, GridColDef } from '@mui/x-data-grid-pro'; -const rows = [ +const items = [ { id: 1, item: 'Paperclip', quantity: 100, price: 1.99 }, { id: 2, item: 'Paper', quantity: 10, price: 30 }, { id: 3, item: 'Pencil', quantity: 100, price: 1.25 }, +]; + +type Item = typeof items[number]; + +interface SubtotalHeader { + id: 'SUBTOTAL'; + label: string; + subtotal: 624; +} + +interface TaxHeader { + id: 'TAX'; + label: string; + taxRate: number; + taxTotal: number; +} + +interface TotalHeader { + id: 'TOTAL'; + label: string; + total: number; +} + +type Row = Item | SubtotalHeader | TaxHeader | TotalHeader; + +const rows: Row[] = [ + ...items, { id: 'SUBTOTAL', label: 'Subtotal', subtotal: 624 }, { id: 'TAX', label: 'Tax', taxRate: 10, taxTotal: 62.4 }, { id: 'TOTAL', label: 'Total', total: 686.4 }, ]; -const columns = [ +const columns: GridColDef[] = [ { field: 'item', headerName: 'Item/Description', @@ -66,14 +93,14 @@ const columns = [ }, ]; -function getCellClassName({ row, field }) { +const getCellClassName: DataGridProProps['getCellClassName'] = ({ row, field }) => { if (row.id === 'SUBTOTAL' || row.id === 'TOTAL' || row.id === 'TAX') { if (field === 'item') { return 'bold'; } } return ''; -} +}; export default function ColumnSpanningFunction() { return ( From 9311848395603113abe0d99a7e9c054d1b6619f0 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 13:02:37 +0200 Subject: [PATCH 26/33] build api docs --- docs/data/data-grid/columns/ColumnSpanningFunction.js | 10 +++++++--- docs/pages/x/api/data-grid/grid-col-def.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.js b/docs/data/data-grid/columns/ColumnSpanningFunction.js index 01ec8f5aab43f..50023edb338f6 100644 --- a/docs/data/data-grid/columns/ColumnSpanningFunction.js +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.js @@ -2,10 +2,14 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGridPro } from '@mui/x-data-grid-pro'; -const rows = [ +const items = [ { id: 1, item: 'Paperclip', quantity: 100, price: 1.99 }, { id: 2, item: 'Paper', quantity: 10, price: 30 }, { id: 3, item: 'Pencil', quantity: 100, price: 1.25 }, +]; + +const rows = [ + ...items, { id: 'SUBTOTAL', label: 'Subtotal', subtotal: 624 }, { id: 'TAX', label: 'Tax', taxRate: 10, taxTotal: 62.4 }, { id: 'TOTAL', label: 'Total', total: 686.4 }, @@ -66,14 +70,14 @@ const columns = [ }, ]; -function getCellClassName({ row, field }) { +const getCellClassName = ({ row, field }) => { if (row.id === 'SUBTOTAL' || row.id === 'TOTAL' || row.id === 'TAX') { if (field === 'item') { return 'bold'; } } return ''; -} +}; export default function ColumnSpanningFunction() { return ( diff --git a/docs/pages/x/api/data-grid/grid-col-def.md b/docs/pages/x/api/data-grid/grid-col-def.md index 737e28419899e..42515730d820f 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.md +++ b/docs/pages/x/api/data-grid/grid-col-def.md @@ -16,7 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | -| colSpan? | number \| ((params: GridCellParams) => number \| undefined) | 1 | Number of columns a grid cell should span. | +| colSpan? | number \| ((params: GridCellParams<V, R, F>) => number \| undefined) | 1 | Number of columns a grid cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | From ab689f2be9df7071b20d40a76bac25c31fa7bfe6 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 13:07:09 +0200 Subject: [PATCH 27/33] update feature comparison --- docs/data/data-grid/getting-started/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/data-grid/getting-started/getting-started.md b/docs/data/data-grid/getting-started/getting-started.md index a99f6e4744e9b..ae1247fbfdc85 100644 --- a/docs/data/data-grid/getting-started/getting-started.md +++ b/docs/data/data-grid/getting-started/getting-started.md @@ -161,7 +161,7 @@ The enterprise components come in two plans: Pro and Premium. | :------------------------------------------------------------------------------------- | :-------: | :--------------------------------: | :----------------------------------------: | | **Column** | | | | [Column groups](/x/react-data-grid/columns/#column-groups) | 🚧 | 🚧 | 🚧 | -| [Column spanning](/x/react-data-grid/columns/#column-spanning) | 🚧 | 🚧 | 🚧 | +| [Column spanning](/x/react-data-grid/columns/#column-spanning) | ✅ | ✅ | ✅ | | [Column resizing](/x/react-data-grid/columns/#resizing) | ❌ | ✅ | ✅ | | [Column reorder](/x/react-data-grid/columns/#column-reorder) | ❌ | ✅ | ✅ | | [Column pinning](/x/react-data-grid/columns/#column-pinning) | ❌ | ✅ | ✅ | From 52819bdff6fe5fcc1c4db16ea39f3c15a2c27739 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 13:27:55 +0200 Subject: [PATCH 28/33] update docs --- docs/data/data-grid/columns/columns.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index 4ae572d1f2758..e425aae9d40c2 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -524,7 +524,7 @@ interface GridColDef { * Number of columns a grid cell should span. * @default 1 */ - colSpan?: number | ((params: GridCellParams) => number | undefined); + colSpan?: number | ((params: GridCellParams) => number | undefined); … } ``` @@ -538,7 +538,7 @@ interface GridColDef { ### Number signature -The `colSpan` number signature allows to span all the cells in the column: +The number signature sets **all cells in the column** to span a given number of columns. ```ts interface GridColDef { @@ -550,11 +550,11 @@ interface GridColDef { ### Function signature -The `colSpan` function signature is useful for spanning only specific cells in the column. The function receives [`GridCellParams`](/api/data-grid/grid-cell-params/) as the first argument: +The function signature allows spanning only **specific cells** in the column. The function receives [`GridCellParams`](/api/data-grid/grid-cell-params/) as an argument: ```ts interface GridColDef { - colSpan?: (params: GridCellParams) => number | undefined; + colSpan?: (params: GridCellParams) => number | undefined; } ``` From 6cb1a464fa1d820e58c2eb95611b66aadcd88fe0 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 16:25:41 +0200 Subject: [PATCH 29/33] update docs --- docs/data/data-grid/columns/columns.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index e425aae9d40c2..9ede38073b13b 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -533,8 +533,9 @@ interface GridColDef { > > - [sorting](/components/data-grid/sorting/#disable-the-sorting) > - [filtering](/components/data-grid/filtering/#disable-the-filters) -> - [column reorder](/components/data-grid/columns/#column-reorder) -> - [hiding columns](/components/data-grid/columns/#column-visibility) +> - [column reorder](#column-reorder) +> - [hiding columns](#column-visibility) +> - [column pinning](#blocking-column-unpinning) ### Number signature From 2236bebc178ab72b6aec82ed9321c3bd3cd9480c Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 16:57:46 +0200 Subject: [PATCH 30/33] disable some features in colSpan demos --- .../columns/ColumnSpanningDerived.js | 3 +++ .../columns/ColumnSpanningDerived.tsx | 3 +++ .../columns/ColumnSpanningDerived.tsx.preview | 1 + .../columns/ColumnSpanningFunction.js | 24 ++++++++++++----- .../columns/ColumnSpanningFunction.tsx | 26 ++++++++++++++----- .../ColumnSpanningFunction.tsx.preview | 2 +- .../data-grid/columns/ColumnSpanningNumber.js | 11 +++++--- .../columns/ColumnSpanningNumber.tsx | 11 +++++--- .../columns/ColumnSpanningNumber.tsx.preview | 12 --------- 9 files changed, 61 insertions(+), 32 deletions(-) delete mode 100644 docs/data/data-grid/columns/ColumnSpanningNumber.tsx.preview diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.js b/docs/data/data-grid/columns/ColumnSpanningDerived.js index f26ec2ff0059d..2ef04bd5ab49e 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.js +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.js @@ -62,6 +62,8 @@ const rows = [ const slotColumnCommonFields = { sortable: false, filterable: false, + pinnable: false, + hideable: false, minWidth: 140, cellClassName: (params) => params.value, colSpan: ({ row, field, value }) => { @@ -176,6 +178,7 @@ export default function ColumnSpanningDerived() { hideFooter showCellRightBorder showColumnRightBorder + disableColumnReorder />
); diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx index 8a6fd5276445b..8273b73319506 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx @@ -71,6 +71,8 @@ const rows: Array<{ id: number; day: string; slots: Array }> = [ const slotColumnCommonFields: Partial = { sortable: false, filterable: false, + pinnable: false, + hideable: false, minWidth: 140, cellClassName: (params: GridCellParams) => params.value, colSpan: ({ row, field, value }: GridCellParams) => { @@ -185,6 +187,7 @@ export default function ColumnSpanningDerived() { hideFooter showCellRightBorder showColumnRightBorder + disableColumnReorder />
); diff --git a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview index 2a9fb9330d5f0..e49f54aef5cdd 100644 --- a/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview +++ b/docs/data/data-grid/columns/ColumnSpanningDerived.tsx.preview @@ -12,4 +12,5 @@ hideFooter showCellRightBorder showColumnRightBorder + disableColumnReorder /> \ No newline at end of file diff --git a/docs/data/data-grid/columns/ColumnSpanningFunction.js b/docs/data/data-grid/columns/ColumnSpanningFunction.js index 50023edb338f6..6faaf4ed26321 100644 --- a/docs/data/data-grid/columns/ColumnSpanningFunction.js +++ b/docs/data/data-grid/columns/ColumnSpanningFunction.js @@ -1,6 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; -import { DataGridPro } from '@mui/x-data-grid-pro'; +import { DataGrid } from '@mui/x-data-grid'; const items = [ { id: 1, item: 'Paperclip', quantity: 100, price: 1.99 }, @@ -15,12 +15,18 @@ const rows = [ { id: 'TOTAL', label: 'Total', total: 686.4 }, ]; +const baseColumnOptions = { + sortable: false, + pinnable: false, + hideable: false, +}; + const columns = [ { field: 'item', headerName: 'Item/Description', + ...baseColumnOptions, flex: 3, - sortable: false, colSpan: ({ row }) => { if (row.id === 'SUBTOTAL' || row.id === 'TOTAL') { return 3; @@ -37,12 +43,18 @@ const columns = [ return value; }, }, - { field: 'quantity', headerName: 'Quantity', flex: 1, sortable: false }, + { + field: 'quantity', + headerName: 'Quantity', + ...baseColumnOptions, + flex: 1, + sortable: false, + }, { field: 'price', headerName: 'Price', flex: 1, - sortable: false, + ...baseColumnOptions, valueGetter: ({ row, value }) => { if (row.id === 'TAX') { return `${row.taxRate}%`; @@ -54,7 +66,7 @@ const columns = [ field: 'total', headerName: 'Total', flex: 1, - sortable: false, + ...baseColumnOptions, valueGetter: ({ row }) => { if (row.id === 'SUBTOTAL') { return row.subtotal; @@ -89,7 +101,7 @@ export default function ColumnSpanningFunction() { }, }} > - [] = [ { field: 'item', headerName: 'Item/Description', + ...baseColumnOptions, flex: 3, - sortable: false, colSpan: ({ row }) => { if (row.id === 'SUBTOTAL' || row.id === 'TOTAL') { return 3; @@ -60,12 +66,18 @@ const columns: GridColDef[] = [ return value; }, }, - { field: 'quantity', headerName: 'Quantity', flex: 1, sortable: false }, + { + field: 'quantity', + headerName: 'Quantity', + ...baseColumnOptions, + flex: 1, + sortable: false, + }, { field: 'price', headerName: 'Price', flex: 1, - sortable: false, + ...baseColumnOptions, valueGetter: ({ row, value }) => { if (row.id === 'TAX') { return `${row.taxRate}%`; @@ -77,7 +89,7 @@ const columns: GridColDef[] = [ field: 'total', headerName: 'Total', flex: 1, - sortable: false, + ...baseColumnOptions, valueGetter: ({ row }) => { if (row.id === 'SUBTOTAL') { return row.subtotal; @@ -93,7 +105,7 @@ const columns: GridColDef[] = [ }, ]; -const getCellClassName: DataGridProProps['getCellClassName'] = ({ row, field }) => { +const getCellClassName: DataGridProps['getCellClassName'] = ({ row, field }) => { if (row.id === 'SUBTOTAL' || row.id === 'TOTAL' || row.id === 'TAX') { if (field === 'item') { return 'bold'; @@ -112,7 +124,7 @@ export default function ColumnSpanningFunction() { }, }} > - \ No newline at end of file From d9b8565cc698f02cf81d5d6af89cb6b20cf1651c Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 17:24:34 +0200 Subject: [PATCH 31/33] code review fixes --- docs/data/data-grid/columns/ColumnSpanningNumber.js | 4 ++-- docs/data/data-grid/columns/ColumnSpanningNumber.tsx | 4 ++-- docs/data/data-grid/columns/columns.md | 5 +++-- docs/pages/x/api/data-grid/grid-col-def.md | 2 +- .../src/tests/columnSpanning.DataGridPro.test.tsx | 3 +-- .../grid/x-data-grid/src/models/api/gridColumnSpanning.ts | 1 - packages/grid/x-data-grid/src/models/colDef/gridColDef.ts | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/data/data-grid/columns/ColumnSpanningNumber.js b/docs/data/data-grid/columns/ColumnSpanningNumber.js index 340d6a303b0be..33282d88bcfe4 100644 --- a/docs/data/data-grid/columns/ColumnSpanningNumber.js +++ b/docs/data/data-grid/columns/ColumnSpanningNumber.js @@ -1,7 +1,7 @@ import * as React from 'react'; import { DataGrid } from '@mui/x-data-grid'; -const otherProps = { +const other = { autoHeight: true, showCellRightBorder: true, showColumnRightBorder: true, @@ -25,7 +25,7 @@ export default function ColumnSpanningNumber() { { id: 1, username: '@MUI', age: 20 }, { id: 2, username: '@MUI-X', age: 25 }, ]} - {...otherProps} + {...other} /> ); diff --git a/docs/data/data-grid/columns/ColumnSpanningNumber.tsx b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx index 340d6a303b0be..33282d88bcfe4 100644 --- a/docs/data/data-grid/columns/ColumnSpanningNumber.tsx +++ b/docs/data/data-grid/columns/ColumnSpanningNumber.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DataGrid } from '@mui/x-data-grid'; -const otherProps = { +const other = { autoHeight: true, showCellRightBorder: true, showColumnRightBorder: true, @@ -25,7 +25,7 @@ export default function ColumnSpanningNumber() { { id: 1, username: '@MUI', age: 20 }, { id: 2, username: '@MUI-X', age: 25 }, ]} - {...otherProps} + {...other} /> ); diff --git a/docs/data/data-grid/columns/columns.md b/docs/data/data-grid/columns/columns.md index 9ede38073b13b..fb3d00e5e8cdb 100644 --- a/docs/data/data-grid/columns/columns.md +++ b/docs/data/data-grid/columns/columns.md @@ -521,7 +521,7 @@ To change the number of columns a cell should span, use the `colSpan` property a ```ts interface GridColDef { /** - * Number of columns a grid cell should span. + * Number of columns a cell should span. * @default 1 */ colSpan?: number | ((params: GridCellParams) => number | undefined); @@ -551,7 +551,8 @@ interface GridColDef { ### Function signature -The function signature allows spanning only **specific cells** in the column. The function receives [`GridCellParams`](/api/data-grid/grid-cell-params/) as an argument: +The function signature allows spanning only **specific cells** in the column. +The function receives [`GridCellParams`](/api/data-grid/grid-cell-params/) as argument. ```ts interface GridColDef { diff --git a/docs/pages/x/api/data-grid/grid-col-def.md b/docs/pages/x/api/data-grid/grid-col-def.md index 42515730d820f..3db4566491512 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.md +++ b/docs/pages/x/api/data-grid/grid-col-def.md @@ -16,7 +16,7 @@ import { GridColDef } from '@mui/x-data-grid'; | :-------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | align? | GridAlignment | | Allows to align the column values in cells. | | cellClassName? | GridCellClassNamePropType | | Class name that will be added in cells for that column. | -| colSpan? | number \| ((params: GridCellParams<V, R, F>) => number \| undefined) | 1 | Number of columns a grid cell should span. | +| colSpan? | number \| ((params: GridCellParams<V, R, F>) => number \| undefined) | 1 | Number of columns a cell should span. | | description? | string | | The description of the column rendered as tooltip if the column header name is not fully displayed. | | disableColumnMenu? | boolean | false | If `true`, the column menu is disabled for this column. | | disableExport? | boolean | false | If `true`, this column will not be included in exports. | diff --git a/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx index c0232dd78cc6f..d3fd73dfdac4f 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columnSpanning.DataGridPro.test.tsx @@ -83,7 +83,6 @@ describe(' - Column Spanning', () => { expect(() => getCell(0, 2)).to.not.throw(); }); - /* eslint-disable material-ui/disallow-active-element-as-key-event-target */ describe('key navigation', () => { const columns: GridColDef[] = [ { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, @@ -110,7 +109,7 @@ describe(' - Column Spanning', () => { apiRef!.current.setColumnIndex('price', 1); fireClickEvent(getCell(1, 1)); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(1, 1), { key: 'ArrowRight' }); expect(getActiveCell()).to.equal('1-2'); }); }); diff --git a/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts b/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts index 34ea6b7da161a..9635c40003553 100644 --- a/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts +++ b/packages/grid/x-data-grid/src/models/api/gridColumnSpanning.ts @@ -4,7 +4,6 @@ import { GridRowId } from '../gridRows'; /** * The Column Spanning API interface that is available in the grid `apiRef`. */ - export interface GridColumnSpanningApi { /** * Returns cell colSpan info. 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 1c3d44be1b83e..f295439eeaba5 100644 --- a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts +++ b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts @@ -226,7 +226,7 @@ export interface GridColDef { */ disableExport?: boolean; /** - * Number of columns a grid cell should span. + * Number of columns a cell should span. * @default 1 */ colSpan?: number | ((params: GridCellParams) => number | undefined); From b782aeb43dc194fb1d4ac87c1c2260565a3b46c2 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 17:28:25 +0200 Subject: [PATCH 32/33] reuse `rowId` variable --- .../features/keyboardNavigation/useGridKeyboardNavigation.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/grid/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/grid/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index d4c0b33e4e5d7..a8c056067f048 100644 --- a/packages/grid/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/grid/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -49,8 +49,7 @@ export const useGridKeyboardNavigation = ( logger.debug(`Navigating to cell row ${rowIndex}, col ${colIndex}`); apiRef.current.scrollToIndexes({ colIndex, rowIndex }); const field = apiRef.current.getVisibleColumns()[colIndex].field; - const node = visibleSortedRows[rowIndex]; - apiRef.current.setCellFocus(node.id, field); + apiRef.current.setCellFocus(rowId, field); }, [apiRef, logger], ); From b5e50d1b0d9c7956563f020695000b26743b1c6a Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskyi Date: Mon, 11 Apr 2022 18:18:51 +0200 Subject: [PATCH 33/33] get rid of document.activeElement in tests --- .../tests/columnSpanning.DataGrid.test.tsx | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx index 4390c17bef358..085870033f9b8 100644 --- a/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx +++ b/packages/grid/x-data-grid/src/tests/columnSpanning.DataGrid.test.tsx @@ -116,7 +116,6 @@ describe(' - Column Spanning', () => { expect(() => getCell(0, 2)).to.not.throw(); }); - /* eslint-disable material-ui/disallow-active-element-as-key-event-target */ describe('key navigation', () => { const columns: GridColDef[] = [ { field: 'brand', colSpan: ({ row }) => (row.brand === 'Nike' ? 2 : 1) }, @@ -135,7 +134,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(0, 0)); expect(getActiveCell()).to.equal('0-0'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(0, 0), { key: 'ArrowRight' }); expect(getActiveCell()).to.equal('0-2'); }); @@ -149,7 +148,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(0, 2)); expect(getActiveCell()).to.equal('0-2'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + fireEvent.keyDown(getCell(0, 2), { key: 'ArrowLeft' }); expect(getActiveCell()).to.equal('0-0'); }); @@ -163,7 +162,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(1, 1)); expect(getActiveCell()).to.equal('1-1'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowUp' }); + fireEvent.keyDown(getCell(1, 1), { key: 'ArrowUp' }); expect(getActiveCell()).to.equal('0-0'); }); @@ -177,7 +176,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(1, 3)); expect(getActiveCell()).to.equal('1-3'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + fireEvent.keyDown(getCell(1, 3), { key: 'ArrowDown' }); expect(getActiveCell()).to.equal('2-2'); }); @@ -196,7 +195,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(0, 3)); expect(getActiveCell()).to.equal('0-3'); - fireEvent.keyDown(document.activeElement!, { key: 'PageDown' }); + fireEvent.keyDown(getCell(0, 3), { key: 'PageDown' }); expect(getActiveCell()).to.equal('2-2'); }); @@ -210,7 +209,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(2, 1)); expect(getActiveCell()).to.equal('2-1'); - fireEvent.keyDown(document.activeElement!, { key: 'PageUp' }); + fireEvent.keyDown(getCell(2, 1), { key: 'PageUp' }); expect(getActiveCell()).to.equal('0-0'); }); @@ -231,10 +230,10 @@ describe(' - Column Spanning', () => { expect(getActiveCell()).to.equal('1-3'); // start editing - fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + fireEvent.keyDown(getCell(1, 3), { key: 'Enter' }); // commit - fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + fireEvent.keyDown(getCell(1, 3).querySelector('input'), { key: 'Enter' }); await waitFor(() => { expect(getActiveCell()).to.equal('2-2'); }); @@ -252,9 +251,9 @@ describe(' - Column Spanning', () => { expect(getActiveCell()).to.equal('1-1'); // start editing - fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + fireEvent.keyDown(getCell(1, 1), { key: 'Enter' }); - fireEvent.keyDown(document.activeElement!, { key: 'Tab' }); + fireEvent.keyDown(getCell(1, 1).querySelector('input'), { key: 'Tab' }); await waitFor(() => { expect(getActiveCell()).to.equal('1-3'); }); @@ -272,9 +271,9 @@ describe(' - Column Spanning', () => { expect(getActiveCell()).to.equal('0-2'); // start editing - fireEvent.keyDown(document.activeElement!, { key: 'Enter' }); + fireEvent.keyDown(getCell(0, 2), { key: 'Enter' }); - fireEvent.keyDown(document.activeElement!, { key: 'Tab', shiftKey: true }); + fireEvent.keyDown(getCell(0, 2).querySelector('input'), { key: 'Tab', shiftKey: true }); await waitFor(() => { expect(getActiveCell()).to.equal('0-0'); }); @@ -335,13 +334,13 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(1, 1)); expect(getActiveCell()).to.equal('1-1'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + fireEvent.keyDown(getCell(1, 1), { key: 'ArrowDown' }); const virtualScroller = document.querySelector(`.${gridClasses.virtualScroller}`)!; // trigger virtualization virtualScroller.dispatchEvent(new Event('scroll')); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + fireEvent.keyDown(getCell(2, 1), { key: 'ArrowDown' }); const activeCell = getActiveCell(); expect(activeCell).to.equal('3-0'); }); @@ -369,7 +368,7 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(0, 0)); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(0, 0), { key: 'ArrowRight' }); document.querySelector(`.${gridClasses.virtualScroller}`)!.dispatchEvent(new Event('scroll')); expect(() => getCell(0, 3)).to.not.throw(); @@ -446,10 +445,10 @@ describe(' - Column Spanning', () => { fireClickEvent(getCell(0, 0)); expect(getActiveCell()).to.equal('0-0'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }); + fireEvent.keyDown(getCell(0, 0), { key: 'ArrowDown' }); expect(getActiveCell()).to.equal('1-0'); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(1, 0), { key: 'ArrowRight' }); expect(getActiveCell()).to.equal('1-2'); }); @@ -481,17 +480,17 @@ describe(' - Column Spanning', () => { `.${gridClasses.virtualScroller}`, )! as HTMLElement; - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(0, 0), { key: 'ArrowRight' }); virtualScroller.dispatchEvent(new Event('scroll')); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' }); + fireEvent.keyDown(getCell(0, 2), { key: 'ArrowRight' }); virtualScroller.dispatchEvent(new Event('scroll')); expect(getActiveCell()).to.equal('0-3'); // should be scrolled to the end of the cell expect(virtualScroller.scrollLeft).to.equal(5 * 100 - virtualScroller.offsetWidth); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + fireEvent.keyDown(getCell(0, 3), { key: 'ArrowLeft' }); virtualScroller.dispatchEvent(new Event('scroll')); - fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' }); + fireEvent.keyDown(getCell(0, 2), { key: 'ArrowLeft' }); virtualScroller.dispatchEvent(new Event('scroll')); expect(getActiveCell()).to.equal('0-0');