diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/data.ts b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/data.ts new file mode 100644 index 000000000000..58c4b8ec31b2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/data.ts @@ -0,0 +1,433 @@ +import { Column } from 'devextreme/ui/data_grid'; + +const dataSource = [{ + ID: 1, + OrderNumber: 35703, + OrderDate: '2014-04-10', + SaleAmount: 11800, + Terms: '15 Days', + TotalAmount: 12175, + CustomerStoreState: 'California', + CustomerStoreCity: 'Los Angeles', + Employee: 'Harv Mudd', +}, { + ID: 4, + OrderNumber: 35711, + OrderDate: '2014-01-12', + SaleAmount: 16050, + Terms: '15 Days', + TotalAmount: 16550, + CustomerStoreState: 'California', + CustomerStoreCity: 'San Jose', + Employee: 'Jim Packard', +}, { + ID: 5, + OrderNumber: 35714, + OrderDate: '2014-01-22', + SaleAmount: 14750, + Terms: '15 Days', + TotalAmount: 15250, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 7, + OrderNumber: 35983, + OrderDate: '2014-02-07', + SaleAmount: 3725, + Terms: '15 Days', + TotalAmount: 3850, + CustomerStoreState: 'Colorado', + CustomerStoreCity: 'Denver', + Employee: 'Todd Hoffman', +}, { + ID: 9, + OrderNumber: 36987, + OrderDate: '2014-03-11', + SaleAmount: 14200, + Terms: '15 Days', + TotalAmount: 14800, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 11, + OrderNumber: 38466, + OrderDate: '2014-03-01', + SaleAmount: 7800, + Terms: '15 Days', + TotalAmount: 8200, + CustomerStoreState: 'California', + CustomerStoreCity: 'Los Angeles', + Employee: 'Harv Mudd', +}, { + ID: 15, + OrderNumber: 39874, + OrderDate: '2014-02-04', + SaleAmount: 9050, + Terms: '30 Days', + TotalAmount: 19100, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 18, + OrderNumber: 42847, + OrderDate: '2014-02-15', + SaleAmount: 20400, + Terms: '30 Days', + TotalAmount: 20800, + CustomerStoreState: 'Wyoming', + CustomerStoreCity: 'Casper', + Employee: 'Todd Hoffman', +}, { + ID: 19, + OrderNumber: 43982, + OrderDate: '2014-05-29', + SaleAmount: 6050, + Terms: '30 Days', + TotalAmount: 6250, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 29, + OrderNumber: 56272, + OrderDate: '2014-02-06', + SaleAmount: 15850, + Terms: '30 Days', + TotalAmount: 16350, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 30, + OrderNumber: 57429, + OrderDate: '2013-12-31', + SaleAmount: 11050, + Terms: '30 Days', + TotalAmount: 11400, + CustomerStoreState: 'Arizona', + CustomerStoreCity: 'Phoenix', + Employee: 'Clark Morgan', +}, { + ID: 32, + OrderNumber: 58292, + OrderDate: '2014-05-13', + SaleAmount: 13500, + Terms: '15 Days', + TotalAmount: 13800, + CustomerStoreState: 'California', + CustomerStoreCity: 'Los Angeles', + Employee: 'Harv Mudd', +}, { + ID: 36, + OrderNumber: 62427, + OrderDate: '2014-01-27', + SaleAmount: 23500, + Terms: '15 Days', + TotalAmount: 24000, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 39, + OrderNumber: 65977, + OrderDate: '2014-02-05', + SaleAmount: 2550, + Terms: '15 Days', + TotalAmount: 2625, + CustomerStoreState: 'Wyoming', + CustomerStoreCity: 'Casper', + Employee: 'Todd Hoffman', +}, { + ID: 40, + OrderNumber: 66947, + OrderDate: '2014-03-23', + SaleAmount: 3500, + Terms: '15 Days', + TotalAmount: 3600, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 42, + OrderNumber: 68428, + OrderDate: '2014-04-10', + SaleAmount: 10500, + Terms: '15 Days', + TotalAmount: 10900, + CustomerStoreState: 'California', + CustomerStoreCity: 'Los Angeles', + Employee: 'Harv Mudd', +}, { + ID: 43, + OrderNumber: 69477, + OrderDate: '2014-03-09', + SaleAmount: 14200, + Terms: '15 Days', + TotalAmount: 14500, + CustomerStoreState: 'California', + CustomerStoreCity: 'Anaheim', + Employee: 'Harv Mudd', +}, { + ID: 46, + OrderNumber: 72947, + OrderDate: '2014-01-14', + SaleAmount: 13350, + Terms: '30 Days', + TotalAmount: 13650, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 47, + OrderNumber: 73088, + OrderDate: '2014-03-25', + SaleAmount: 8600, + Terms: '30 Days', + TotalAmount: 8850, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Reno', + Employee: 'Clark Morgan', +}, { + ID: 50, + OrderNumber: 76927, + OrderDate: '2014-04-27', + SaleAmount: 9800, + Terms: '30 Days', + TotalAmount: 10050, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 51, + OrderNumber: 77297, + OrderDate: '2014-04-30', + SaleAmount: 10850, + Terms: '30 Days', + TotalAmount: 11100, + CustomerStoreState: 'Arizona', + CustomerStoreCity: 'Phoenix', + Employee: 'Clark Morgan', +}, { + ID: 56, + OrderNumber: 84744, + OrderDate: '2014-02-10', + SaleAmount: 4650, + Terms: '30 Days', + TotalAmount: 4750, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 57, + OrderNumber: 85028, + OrderDate: '2014-05-17', + SaleAmount: 2575, + Terms: '30 Days', + TotalAmount: 2625, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Reno', + Employee: 'Clark Morgan', +}, { + ID: 59, + OrderNumber: 87297, + OrderDate: '2014-04-21', + SaleAmount: 14200, + Terms: '30 Days', + TotalAmount: 0, + CustomerStoreState: 'Wyoming', + CustomerStoreCity: 'Casper', + Employee: 'Todd Hoffman', +}, { + ID: 60, + OrderNumber: 88027, + OrderDate: '2014-02-14', + SaleAmount: 13650, + Terms: '30 Days', + TotalAmount: 14050, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 65, + OrderNumber: 94726, + OrderDate: '2014-05-22', + SaleAmount: 20500, + Terms: '15 Days', + TotalAmount: 20800, + CustomerStoreState: 'California', + CustomerStoreCity: 'San Jose', + Employee: 'Jim Packard', +}, { + ID: 66, + OrderNumber: 95266, + OrderDate: '2014-03-10', + SaleAmount: 9050, + Terms: '15 Days', + TotalAmount: 9250, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 69, + OrderNumber: 98477, + OrderDate: '2014-01-01', + SaleAmount: 23500, + Terms: '15 Days', + TotalAmount: 23800, + CustomerStoreState: 'Wyoming', + CustomerStoreCity: 'Casper', + Employee: 'Todd Hoffman', +}, { + ID: 70, + OrderNumber: 99247, + OrderDate: '2014-02-08', + SaleAmount: 2100, + Terms: '15 Days', + TotalAmount: 2150, + CustomerStoreState: 'Utah', + CustomerStoreCity: 'Salt Lake City', + Employee: 'Clark Morgan', +}, { + ID: 78, + OrderNumber: 174884, + OrderDate: '2014-04-10', + SaleAmount: 7200, + Terms: '30 Days', + TotalAmount: 7350, + CustomerStoreState: 'Colorado', + CustomerStoreCity: 'Denver', + Employee: 'Todd Hoffman', +}, { + ID: 81, + OrderNumber: 188877, + OrderDate: '2014-02-11', + SaleAmount: 8750, + Terms: '30 Days', + TotalAmount: 8900, + CustomerStoreState: 'Arizona', + CustomerStoreCity: 'Phoenix', + Employee: 'Clark Morgan', +}, { + ID: 82, + OrderNumber: 191883, + OrderDate: '2014-02-05', + SaleAmount: 9900, + Terms: '30 Days', + TotalAmount: 10150, + CustomerStoreState: 'California', + CustomerStoreCity: 'Los Angeles', + Employee: 'Harv Mudd', +}, { + ID: 83, + OrderNumber: 192474, + OrderDate: '2014-01-21', + SaleAmount: 12800, + Terms: '30 Days', + TotalAmount: 13100, + CustomerStoreState: 'California', + CustomerStoreCity: 'Anaheim', + Employee: 'Harv Mudd', +}, { + ID: 84, + OrderNumber: 193847, + OrderDate: '2014-03-21', + SaleAmount: 14100, + Terms: '30 Days', + TotalAmount: 14350, + CustomerStoreState: 'California', + CustomerStoreCity: 'San Diego', + Employee: 'Harv Mudd', +}, { + ID: 85, + OrderNumber: 194877, + OrderDate: '2014-03-06', + SaleAmount: 4750, + Terms: '30 Days', + TotalAmount: 4950, + CustomerStoreState: 'California', + CustomerStoreCity: 'San Jose', + Employee: 'Jim Packard', +}, { + ID: 86, + OrderNumber: 195746, + OrderDate: '2014-05-26', + SaleAmount: 9050, + Terms: '30 Days', + TotalAmount: 9250, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Las Vegas', + Employee: 'Harv Mudd', +}, { + ID: 87, + OrderNumber: 197474, + OrderDate: '2014-03-02', + SaleAmount: 6400, + Terms: '30 Days', + TotalAmount: 6600, + CustomerStoreState: 'Nevada', + CustomerStoreCity: 'Reno', + Employee: 'Clark Morgan', +}, { + ID: 88, + OrderNumber: 198746, + OrderDate: '2014-05-09', + SaleAmount: 15700, + Terms: '30 Days', + TotalAmount: 16050, + CustomerStoreState: 'Colorado', + CustomerStoreCity: 'Denver', + Employee: 'Todd Hoffman', +}, { + ID: 91, + OrderNumber: 214222, + OrderDate: '2014-02-08', + SaleAmount: 11050, + Terms: '30 Days', + TotalAmount: 11250, + CustomerStoreState: 'Arizona', + CustomerStoreCity: 'Phoenix', + Employee: 'Clark Morgan', +}]; + +const columns: Column[] = [{ + dataField: 'OrderNumber', + width: 130, + caption: 'Invoice Number', + fixed: true, + fixedPosition: 'left', +}, { + dataField: 'OrderDate', + dataType: 'date', +}, { + dataField: 'Employee', +}, { + caption: 'City', + dataField: 'CustomerStoreCity', +}, { + caption: 'State', + dataField: 'CustomerStoreState', +}, { + dataField: 'SaleAmount', + width: 160, + alignment: 'right', + format: 'currency', +}, { + dataField: 'TotalAmount', + width: 160, + alignment: 'right', + format: 'currency', + fixed: true, + fixedPosition: 'right', +}]; + +export const defaultConfig = { + width: 700, + height: 500, + dataSource, + columnAutoWidth: true, + keyExpr: 'ID', + columns, +}; diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-begin.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-begin.png new file mode 100644 index 000000000000..1be7c06209f3 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-begin.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-center.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-center.png new file mode 100644 index 000000000000..f7ee16b30e72 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-center.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-end.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-end.png new file mode 100644 index 000000000000..f113a02b6f10 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/grouping-scroll-end.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-begin.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-begin.png new file mode 100644 index 000000000000..527851e7d865 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-begin.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-center.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-center.png new file mode 100644 index 000000000000..f2629c972aa5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-center.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-end.png b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-end.png new file mode 100644 index 000000000000..2fce35681176 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/etalons/masterdetail-scroll-end.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withGrouping.ts b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withGrouping.ts new file mode 100644 index 000000000000..f88703736b8e --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withGrouping.ts @@ -0,0 +1,77 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { safeSizeTest } from '../../../helpers/safeSizeTest'; +import { createWidget } from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; +import { defaultConfig } from './data'; + +const DATA_GRID_SELECTOR = '#container'; + +fixture.disablePageReloads`FixedColumns - Grouping` + .page(url(__dirname, '../../container.html')); + +safeSizeTest('Sticky columns with grouping & summary', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + + await takeScreenshot('grouping-scroll-begin.png', dataGrid.element); + + await dataGrid.scrollTo(t, { x: 100 }); + await takeScreenshot('grouping-scroll-center.png', dataGrid.element); + + await dataGrid.scrollTo(t, { x: 10000 }); + await takeScreenshot('grouping-scroll-end.png', dataGrid.element); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}, [900, 800]).before(async () => createWidget('dxDataGrid', { + ...defaultConfig, + customizeColumns(columns) { + columns[2].groupIndex = 0; + }, + summary: { + groupItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'City', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + showInGroupFooter: false, + alignByColumn: true, + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + showInGroupFooter: true, + }], + totalItems: [{ + column: 'OrderNumber', + summaryType: 'count', + displayFormat: '{0} orders', + }, { + column: 'SaleAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'max', + valueFormat: 'currency', + }, { + column: 'TotalAmount', + summaryType: 'sum', + valueFormat: 'currency', + displayFormat: 'Total: {0}', + }], + }, +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withMasterDetail.ts b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withMasterDetail.ts new file mode 100644 index 000000000000..ec5b1e9c172f --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/stickyColumns/withMasterDetail.ts @@ -0,0 +1,39 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { safeSizeTest } from '../../../helpers/safeSizeTest'; +import { createWidget } from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; +import { defaultConfig } from './data'; + +const DATA_GRID_SELECTOR = '#container'; + +fixture.disablePageReloads`FixedColumns - MasterDetail` + .page(url(__dirname, '../../container.html')); + +safeSizeTest('Sticky columns with master-detail', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + await dataGrid.apiExpandRow(1); + + await takeScreenshot('masterdetail-scroll-begin.png', dataGrid.element); + + await dataGrid.scrollTo(t, { x: 100 }); + await takeScreenshot('masterdetail-scroll-center.png', dataGrid.element); + + await dataGrid.scrollTo(t, { x: 10000 }); + await takeScreenshot('masterdetail-scroll-end.png', dataGrid.element); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}, [900, 800]).before(async () => createWidget('dxDataGrid', { + ...defaultConfig, + masterDetail: { + enabled: true, + template(container) { + $(container) + .text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); + }, + }, +})); diff --git a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss index 8248fed98967..4ac34a999dca 100644 --- a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss +++ b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss @@ -954,6 +954,12 @@ &:focus { outline: 0; } + + &.dx-#{$widget-name}-sticky-column-left { + display: inline-block; + overflow: hidden; + left: 0; + } } .dx-data-row.dx-edit-row { @@ -994,9 +1000,9 @@ .dx-#{$widget-name}-total-footer { position: relative; - & > .dx-#{$widget-name}-content { - padding-top: $grid-total-footer-paddings; - padding-bottom: $grid-total-footer-paddings; + & > .dx-#{$widget-name}-content .dx-row > td { + padding-top: $grid-total-footer-paddings * 2; + padding-bottom: $grid-total-footer-paddings * 2; } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/dataGrid/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/dataGrid/_index.scss index 76e14c67e65b..4d7ae88d96bc 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/dataGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/dataGrid/_index.scss @@ -67,12 +67,31 @@ } .dx-datagrid-rowsview { + .dx-datagrid-group-row-container { + padding-left: $fluent-grid-base-cell-horizontal-padding; + padding-right: $fluent-grid-base-cell-horizontal-padding; + text-overflow: ellipsis; + display: inline-block; + position: sticky; + background-color: $datagrid-group-row-bg; + } + .dx-row.dx-group-row:first-child { border-top: none; } .dx-row.dx-group-row { + .dx-group-cell { + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + padding-left: 0; + padding-right: 0; + overflow: initial; + overflow-x: clip; + background-color: transparent; + } + } + &:not(.dx-row-focused) { color: $datagrid-group-row-color; background-color: $datagrid-group-row-bg; @@ -84,6 +103,10 @@ td { border-top-color: $fluent-grid-base-border-color; border-bottom-color: $fluent-grid-base-border-color; + + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + background-color: $datagrid-group-row-bg; + } } } } @@ -123,3 +146,4 @@ } } } + diff --git a/packages/devextreme-scss/scss/widgets/generic/dataGrid/_index.scss b/packages/devextreme-scss/scss/widgets/generic/dataGrid/_index.scss index 6db086a6a8b0..2c2a275f277c 100644 --- a/packages/devextreme-scss/scss/widgets/generic/dataGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/dataGrid/_index.scss @@ -66,6 +66,16 @@ } .dx-row.dx-group-row { + .dx-group-cell { + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + padding-left: 0; + padding-right: 0; + overflow: initial; + overflow-x: clip; + background-color: transparent; + } + } + &:not(.dx-row-focused) { color: $datagrid-group-row-color; background-color: $datagrid-group-row-bg; @@ -76,8 +86,21 @@ td { border-top-color: $generic-grid-base-border-color; border-bottom-color: $generic-grid-base-border-color; + + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + background-color: $datagrid-group-row-bg; + } } } + + .dx-datagrid-group-row-container { + padding-left: $generic-datagrid-cell-padding; + padding-right: $generic-datagrid-cell-padding; + text-overflow: ellipsis; + display: inline-block; + position: sticky; + background-color: $datagrid-group-row-bg; + } } .dx-datagrid-group-opened { diff --git a/packages/devextreme-scss/scss/widgets/material/dataGrid/_index.scss b/packages/devextreme-scss/scss/widgets/material/dataGrid/_index.scss index da8a035a3aef..d1136950eebb 100644 --- a/packages/devextreme-scss/scss/widgets/material/dataGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/dataGrid/_index.scss @@ -64,12 +64,31 @@ } .dx-datagrid-rowsview { + .dx-datagrid-group-row-container { + padding-left: $material-grid-base-cell-horizontal-padding; + padding-right: $material-grid-base-cell-horizontal-padding; + text-overflow: ellipsis; + display: inline-block; + position: sticky; + background-color: $datagrid-group-row-bg; + } + .dx-row.dx-group-row:first-child { border-top: none; } .dx-row.dx-group-row { + .dx-group-cell { + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + padding-left: 0; + padding-right: 0; + overflow: initial; + overflow-x: clip; + background-color: transparent; + } + } + &:not(.dx-row-focused) { color: $datagrid-group-row-color; background-color: $datagrid-group-row-bg; @@ -81,6 +100,10 @@ td { border-top-color: $material-grid-base-border-color; border-bottom-color: $material-grid-base-border-color; + + &.dx-datagrid-sticky-column, &.dx-datagrid-sticky-column-left, &.dx-datagrid-sticky-column-right { + background-color: $datagrid-group-row-bg; + } } } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 4434da8069a1..1e0a638b3df8 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -8,7 +8,7 @@ import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; -import { getHeight } from '@js/core/utils/size'; +import { getHeight, getInnerWidth, getOuterWidth } from '@js/core/utils/size'; import { format } from '@js/core/utils/string'; import { isDefined, isFunction, isString } from '@js/core/utils/type'; import variableWrapper from '@js/core/utils/variable_wrapper'; @@ -781,4 +781,12 @@ export default { logSpecificDeprecatedWarningIfNeed(columns); }, + + getComponentBorderWidth(that, $rowsViewElement) { + const borderWidth = that.option('showBorders') + ? Math.ceil(getOuterWidth($rowsViewElement) - getInnerWidth($rowsViewElement)) + : 0; + + return borderWidth; + }, }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts index 6bc68b783261..b8c03fb566b3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; // @ts-expect-error import { grep } from '@js/core/utils/common'; @@ -339,7 +340,7 @@ const rowsView = (Base: ModuleType) => class RowsViewMasterDetailExten return template; } - private _isDetailRow(row) { + protected _isDetailRow(row) { return row && row.rowType && row.rowType.indexOf('detail') === 0; } @@ -359,35 +360,40 @@ const rowsView = (Base: ModuleType) => class RowsViewMasterDetailExten protected _renderCells($row, options) { const { row } = options; - let $detailCell; - const visibleColumns = this._columnsController.getVisibleColumns(); if (row.rowType && this._isDetailRow(row)) { if (this._needRenderCell(0, options.columnIndices)) { - $detailCell = this._renderCell($row, { - value: null, - row, - rowIndex: row.rowIndex, - column: { command: 'detail' }, - columnIndex: 0, - change: options.change, - }); - - $detailCell - .addClass(CELL_FOCUS_DISABLED_CLASS) - .addClass(MASTER_DETAIL_CELL_CLASS) - .attr('colSpan', visibleColumns.length); - - const isEditForm = row.isEditing; - - if (!isEditForm) { - $detailCell.attr('aria-roledescription', messageLocalization.format('dxDataGrid-masterDetail')); - } + this._renderMasterDetailCell($row, row, options); } } else { super._renderCells.apply(this, arguments as any); } } + protected _renderMasterDetailCell($row, row, options): dxElementWrapper { + const visibleColumns = this._columnsController.getVisibleColumns(); + + const $detailCell = this._renderCell($row, { + value: null, + row, + rowIndex: row.rowIndex, + column: { command: 'detail' }, + columnIndex: 0, + change: options.change, + }); + + $detailCell + .addClass(CELL_FOCUS_DISABLED_CLASS) + .addClass(MASTER_DETAIL_CELL_CLASS) + .attr('colSpan', visibleColumns.length); + + const isEditForm = row.isEditing; + + if (!isEditForm) { + $detailCell.attr('aria-roledescription', messageLocalization.format('dxDataGrid-masterDetail')); + } + + return $detailCell; + } }; export const masterDetailModule = { diff --git a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/const.ts b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/const.ts index 7cdec1ba81b7..0f531b099cf4 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/const.ts @@ -15,4 +15,5 @@ export const CLASSES = { stickyColumns: 'sticky-columns', firstHeader: 'first-header', columnNoBorder: 'column-no-border', + groupRowContainer: 'group-row-container', }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/dom.ts b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/dom.ts index 0a68a37c8dc2..095f65fcf2a2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/dom.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/dom.ts @@ -260,12 +260,24 @@ const noNeedToCreateReorderingPoint = ( return point.x < nonFixedAreaBoundingRect.left || point.x > nonFixedAreaBoundingRect.right; }; +const doesGroupCellEndInFirstColumn = ($groupCell): boolean => { + const $groupRow = $groupCell.parent(); + const commandColumns = $groupRow.children().filter( + (i) => i < $groupCell.index(), + ); + + const groupColSpanWithoutCommand = $groupCell.attr('colspan') - commandColumns.length; + + return groupColSpanWithoutCommand === 1; +}; + export const GridCoreStickyColumnsDom = { addFirstHeaderClass, addColumnNoBorderClass, addStickyColumnClass, addStickyColumnBorderLeftClass, addStickyColumnBorderRightClass, + doesGroupCellEndInFirstColumn, toggleStickyColumnsClass, getLeftFixedCells, getRightFixedCells, diff --git a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/m_sticky_columns.ts b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/m_sticky_columns.ts index 39dfdc6e6eb2..7384d582f159 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/m_sticky_columns.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/m_sticky_columns.ts @@ -1,6 +1,7 @@ /* eslint-disable max-classes-per-file */ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import { setWidth } from '@js/core/utils/size'; import type { ColumnHeadersView } from '../column_headers/m_column_headers'; import type { @@ -8,9 +9,11 @@ import type { DraggingHeaderViewController, } from '../columns_resizing_reordering/m_columns_resizing_reordering'; import type { ModuleType } from '../m_types'; +import gridCoreUtils from '../m_utils'; import type { ColumnsView } from '../views/m_columns_view'; import type { RowsView } from '../views/m_rows_view'; -import { StickyPosition } from './const'; +import { isGroupRow } from '../views/m_rows_view'; +import { CLASSES, StickyPosition } from './const'; import { GridCoreStickyColumnsDom } from './dom'; import { getColumnFixedPosition, @@ -152,7 +155,7 @@ const baseStickyColumns = >(Base: T) => class const styleProps = normalizeOffset(offset); - this.setCellProperties(styleProps, visibleColumnIndex, rowIndex); + this.setCellProperties(styleProps, visibleColumnIndex, rowIndex, true); } }); } @@ -282,7 +285,70 @@ const columnHeadersView = ( const rowsView = ( Base: ModuleType, -) => class RowsViewStickyColumnsExtender extends baseStickyColumns(Base) {}; +) => class RowsViewStickyColumnsExtender extends baseStickyColumns(Base) { + private _getMasterDetailWidth(): number { + // @ts-expect-error + const componentWidth = this.component.$element().width?.() ?? 0; + return componentWidth - gridCoreUtils.getComponentBorderWidth(this, this._$element); + } + + protected _renderMasterDetailCell($row, row, options): dxElementWrapper { + // @ts-expect-error + const $detailCell: dxElementWrapper = super._renderMasterDetailCell($row, row, options); + + if (this._isStickyColumns()) { + $detailCell + .addClass(this.addWidgetPrefix(CLASSES.stickyColumnLeft)) + // @ts-expect-error + .width(this._getMasterDetailWidth()); + } + + return $detailCell; + } + + private _updateMasterDetailWidths() { + setWidth( + this._$element?.find('.dx-master-detail-cell'), + this._getMasterDetailWidth(), + ); + } + + protected _resizeCore() { + const isStickyColumns = this._isStickyColumns(); + + super._resizeCore.apply(this, arguments as any); + + if (isStickyColumns) { + this._updateMasterDetailWidths(); + } + } + + protected _renderCellContent($cell, options, renderOptions) { + if (!isGroupRow(options) || !this._isStickyColumns()) { + return super._renderCellContent($cell, options, renderOptions); + } + + const $container = $('
') + .addClass(this.addWidgetPrefix(CLASSES.groupRowContainer)) + .appendTo($cell); + + return super._renderCellContent($container, options, renderOptions); + } + + protected _renderGroupSummaryCellsCore($groupCell, options, groupCellColSpan, alignByColumnCellCount) { + // @ts-expect-error + super._renderGroupSummaryCellsCore($groupCell, options, groupCellColSpan, alignByColumnCellCount); + const stickySummarySelector = `.${this.addWidgetPrefix(CLASSES.stickyColumn)}`; + + if ( + $groupCell.parent().find(stickySummarySelector).length + && GridCoreStickyColumnsDom.doesGroupCellEndInFirstColumn($groupCell) + ) { + GridCoreStickyColumnsDom + .addStickyColumnBorderRightClass($groupCell, this.addWidgetPrefix.bind(this)); + } + } +}; const footerView = ( Base: ModuleType, diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 76796022b57c..e7a5694f9506 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -52,6 +52,7 @@ const DETAIL_ROW_CLASS = 'dx-master-detail-row'; const FILTER_ROW_CLASS = 'filter-row'; const ERROR_ROW_CLASS = 'dx-error-row'; const CELL_UPDATED_ANIMATION_CLASS = 'cell-updated-animation'; +const GROUP_ROW_CONTAINER = 'group-row-container'; const HIDDEN_COLUMNS_WIDTH = '0.0001px'; @@ -1195,19 +1196,38 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { return columnIndex; } - private setCellPropertiesCore(styleProps: CSSStyleDeclaration, $row, visibleCellIndex) { - const $cell = $row.hasClass(GROUP_ROW_CLASS) - ? $row.find(`td[aria-colindex='${visibleCellIndex + 1}']:not(.${GROUP_CELL_CLASS})`) + private setCellPropertiesCore( + styleProps: CSSStyleDeclaration, + $row: dxElementWrapper, + visibleCellIndex: number, + includeGroupCell: boolean, + ) { + const groupSelector = includeGroupCell + ? `td[aria-colindex='${visibleCellIndex + 1}']` + : `td[aria-colindex='${visibleCellIndex + 1}']:not(.${GROUP_CELL_CLASS})`; + + let $cell = $row.hasClass(GROUP_ROW_CLASS) + ? $row.find(groupSelector) : $row.find('td').eq(visibleCellIndex); - if ($cell.length) { - const cell = $cell.get(0) as HTMLElement; + if ($cell.is(`.${GROUP_CELL_CLASS}`)) { + // @ts-expect-error + $cell = $cell.add($cell.find(`.${this.addWidgetPrefix(GROUP_ROW_CONTAINER)}`)); + } + + for (let i = 0; i < $cell.length; i += 1) { + const cell = $cell.get(i) as HTMLElement; Object.assign(cell.style, styleProps); } } - protected setCellProperties(styleProps: CSSStyleDeclaration, columnIndex: number, rowIndex?: number) { + protected setCellProperties( + styleProps: CSSStyleDeclaration, + columnIndex: number, + rowIndex?: number, + includeGroupCell = false, + ) { const $tableElement = this.getTableElement(); if (!$tableElement?.length) { @@ -1217,13 +1237,13 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { const $rows = $tableElement.children().children('.dx-row').not(`.${DETAIL_ROW_CLASS}`); if (isDefined(rowIndex)) { - this.setCellPropertiesCore(styleProps, $rows.eq(rowIndex), columnIndex); + this.setCellPropertiesCore(styleProps, $rows.eq(rowIndex), columnIndex, includeGroupCell); } else { for (let rowIndex = 0; rowIndex < $rows.length; rowIndex++) { const visibleIndex = this.getVisibleColumnIndex(columnIndex, rowIndex); if (visibleIndex >= 0) { - this.setCellPropertiesCore(styleProps, $rows.eq(rowIndex), visibleIndex); + this.setCellPropertiesCore(styleProps, $rows.eq(rowIndex), visibleIndex, includeGroupCell); } } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 845f337d3ec7..8dd3599d6576 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -8,10 +8,7 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; -import { - getHeight, - getInnerWidth, getOuterWidth, getWidth, -} from '@js/core/utils/size'; +import { getHeight, getWidth } from '@js/core/utils/size'; import { isDefined, isNumeric, isString } from '@js/core/utils/type'; import { getWindow, hasWindow } from '@js/core/utils/window'; import messageLocalization from '@js/localization/message'; @@ -508,9 +505,7 @@ export class ResizingController extends modules.ViewController { resultWidths[lastColumnIndex] = 'auto'; isColumnWidthsCorrected = true; if (hasWidth === false && !hasPercentWidth) { - const borderWidth = that.option('showBorders') - ? Math.ceil(getOuterWidth($rowsViewElement) - getInnerWidth($rowsViewElement)) - : 0; + const borderWidth = gridCoreUtils.getComponentBorderWidth(this, $rowsViewElement); that._maxWidth = totalWidth + scrollbarWidth + borderWidth; // @ts-expect-error diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts index e7e302ba64d2..21735ad24f22 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts @@ -30,6 +30,7 @@ import type { ResizingController } from '@ts/grids/grid_core/views/m_grid_view'; import type { EditingController } from '../editing/m_editing'; import type { EditorFactory } from '../editor_factory/m_editor_factory'; import gridCoreUtils from '../m_utils'; +import { CLASSES } from '../sticky_columns/const'; import { ColumnsView } from './m_columns_view'; const ROWS_VIEW_CLASS = 'rowsview'; @@ -52,7 +53,7 @@ const LOADPANEL_HIDE_TIMEOUT = 200; function getMaxHorizontalScrollOffset(scrollable) { return scrollable ? Math.round(scrollable.scrollWidth() - scrollable.clientWidth()) : 0; } -function isGroupRow({ rowType, column }) { +export function isGroupRow({ rowType, column }) { return rowType === 'group' && isDefined(column.groupIndex) && !column.showWhenGrouped && !column.command; @@ -212,7 +213,12 @@ export class RowsView extends ColumnsView { */ public _updateCell($cell, options) { if (isGroupRow(options)) { - $cell.addClass(GROUP_CELL_CLASS); + const isGroupContainer = $cell.is(`.${this.addWidgetPrefix(CLASSES.groupRowContainer)}`); + const $groupCell = isGroupContainer + ? $cell.parent() + : $cell; + + $groupCell.addClass(GROUP_CELL_CLASS); } super._updateCell.apply(this, arguments as any); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnFixing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnFixing.tests.js index ecafa1b60170..c97718c7daa9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnFixing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnFixing.tests.js @@ -983,10 +983,10 @@ QUnit.module('Fixed columns', { assert.strictEqual($fixTable.find('tbody > tr').first().find('td').last().html(), '', 'fixed column'); // T445226 - assert.equal($footerContentElements.filter(':not(.dx-datagrid-content-fixed)').css('padding-top'), '7px', 'padding top of main content'); - assert.equal($footerContentElements.filter(':not(.dx-datagrid-content-fixed)').css('paddingBottom'), '7px', 'padding bottom of main content'); - assert.equal($footerContentElements.filter('.dx-datagrid-content-fixed').css('padding-top'), '7px', 'padding top of fixed content'); - assert.equal($footerContentElements.filter('.dx-datagrid-content-fixed').css('paddingBottom'), '7px', 'padding bottom of fixed content'); + assert.equal($footerContentElements.filter(':not(.dx-datagrid-content-fixed)').find('td').css('padding-top'), '14px', 'padding top of main content'); + assert.equal($footerContentElements.filter(':not(.dx-datagrid-content-fixed)').find('td').css('paddingBottom'), '14px', 'padding bottom of main content'); + assert.equal($footerContentElements.filter('.dx-datagrid-content-fixed').find('td').css('padding-top'), '14px', 'padding top of fixed content'); + assert.equal($footerContentElements.filter('.dx-datagrid-content-fixed').find('td').css('paddingBottom'), '14px', 'padding bottom of fixed content'); }); // T232872