diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts new file mode 100644 index 000000000000..2246ffd71108 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts @@ -0,0 +1,123 @@ +import Button from 'devextreme-testcafe-models/button'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Band columns.Functional` + .page(url(__dirname, '../../../container.html')); + +const GRID_CONTAINER = '#container'; + +test('Changing dataField for a banded column with the columnOption method does not work as expected (T1210340)', async (t) => { + const dataGrid = new DataGrid(GRID_CONTAINER); + const changeFieldButton = new Button('#otherContainer'); + + await t + .expect(dataGrid.getDataCell(0, 4).element.innerText) + .eql('2353025') + .click(changeFieldButton.element) + .expect(dataGrid.getDataCell(0, 4).element.innerText) + .eql('0.672'); +}).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [{ + id: 1, + Country: 'Brazil', + Area: 8515767, + Population_Urban: 0.85, + Population_Rural: 0.15, + Population_Total: 205809000, + GDP_Agriculture: 0.054, + GDP_Industry: 0.274, + GDP_Services: 0.672, + GDP_Total: 2353025, + }], + columns: [ + 'Country', + 'Area', { + caption: 'Population', + columns: [ + 'Population_Total', + 'Population_Urban', + ], + }, { + caption: 'Nominal GDP', + columns: [{ + caption: 'Total, mln $', + dataField: 'GDP_Total', + name: 'GDP_Total', + }, { + caption: 'By Sector', + columns: [{ + caption: 'Agriculture', + dataField: 'GDP_Agriculture', + }, { + caption: 'Industry', + dataField: 'GDP_Industry', + format: { + type: 'percent', + }, + }, { + caption: 'Services', + dataField: 'GDP_Services', + }], + }], + }], + keyExpr: 'id', + showBorders: true, + }); + + await createWidget('dxButton', { + text: 'Change fields', + onClick() { + const grid = ($('#container') as any).dxDataGrid('instance'); + grid.columnOption('GDP_Total', 'dataField', 'GDP_Services'); + }, + }, '#otherContainer'); +}); + +test('The first header class should update correctly when the first data column is hidden in responsive mode', async (t) => { + const dataGrid = new DataGrid(GRID_CONTAINER); + const firstHeaderRow = dataGrid.getHeaders().getHeaderRow(0); + const secondHeaderRow = dataGrid.getHeaders().getHeaderRow(1); + + await t + .expect(dataGrid.isReady()).ok() + .expect(firstHeaderRow.getHeaderCell(0).isFirstHeader) + .ok() + .expect(firstHeaderRow.getHeaderCell(2).isFirstHeader) + .notOk() + .expect(secondHeaderRow.getHeaderCell(0).isFirstHeader) + .ok() + .expect(secondHeaderRow.getHeaderCell(1).isFirstHeader) + .notOk(); + + await dataGrid.apiOption('width', 275); + + await t + .expect(firstHeaderRow.getHeaderCell(0).isFirstHeader) + .ok() + .expect(firstHeaderRow.getHeaderCell(2).isFirstHeader) + .notOk() + .expect(secondHeaderRow.getHeaderCell(0).isFirstHeader) + .notOk() + .expect(secondHeaderRow.getHeaderCell(1).isFirstHeader) + .ok(); +}).before(async () => { + await createWidget('dxDataGrid', { + width: 350, + columnWidth: 100, + columnHidingEnabled: true, + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + { + caption: 'Band 1', + columns: [ + { dataField: 'field1', hidingPriority: 0 }, + { dataField: 'field2', hidingPriority: 1 }, + ], + }, + { dataField: 'field3', hidingPriority: 2 }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.ts new file mode 100644 index 000000000000..0b758f4a5131 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.ts @@ -0,0 +1,174 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; +import { testScreenshot } from '../../../../helpers/themeUtils'; +import { Themes } from '../../../../helpers/themes'; + +fixture.disablePageReloads`Band columns.Matrix` + .page(url(__dirname, '../../../container.html')); + +const GRID_CONTAINER = '#container'; + +const configs = [{ + showColumnLines: true, + rtlEnabled: false, +}, { + showColumnLines: true, + rtlEnabled: true, +}, { + showColumnLines: false, + rtlEnabled: false, +}, { + showColumnLines: false, + rtlEnabled: true, +}]; + +configs.forEach(( + { showColumnLines, rtlEnabled }: { showColumnLines: boolean; rtlEnabled: boolean; }, +): void => { + test.meta({ themes: [Themes.materialBlue, Themes.genericLight] })(`The grid with grouped and fixed columns should display correct vertical borders when showColumnLines=${showColumnLines} and rtl=${rtlEnabled}(T1318812)`, async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const dataGrid = new DataGrid(GRID_CONTAINER); + + await t.expect(dataGrid.isReady()).ok(); + + await testScreenshot( + t, + takeScreenshot, + `T1318812__datagrid__band-and-grouped-and-fixed-columns__vertical-borders(showColumnLines=${showColumnLines},rtl=${rtlEnabled}).png`, + { element: dataGrid.element }, + ); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [ + { + Col1: 'Data A', Col2: 'Desc A', Col3: 'Group 1', Col4: 'X', Col5: 100, Col6: 50, + }, + { + Col1: 'Data B', Col2: 'Desc B', Col3: 'Group 1', Col4: 'Y', Col5: 200, Col6: 20, + }, + { + Col1: 'Data C', Col2: 'Desc C', Col3: 'Group 2', Col4: 'Z', Col5: 300, Col6: 10, + }, + ], + columns: [ + { + caption: 'Band Column 1', + columns: [ + { + caption: 'Nested BandColumn 1', + columns: [ + { dataField: 'Col1', width: 150 }, + { dataField: 'Col2', width: 300 }, + { dataField: 'Col3', width: 300, groupIndex: 0 }, + ], + }, + ], + }, + { + caption: 'Band Column 2', + fixed: true, + columns: [ + { + caption: 'Nested Band Column 2', + columns: [ + { dataField: 'Col4', width: 120 }, + ], + }, + ], + }, + { + caption: 'Band Column 3', + columns: [ + { + caption: 'Nested Band Column 3', + columns: [ + { dataField: 'Col5', width: 150 }, + { dataField: 'Col6', width: 150 }, + ], + }, + ], + }, + ], + showColumnLines, + rtlEnabled, + columnWidth: 100, + }); + }); + + test.meta({ themes: [Themes.materialBlue, Themes.genericLight] })(`The grid should display correct vertical borders when showColumnLines=${showColumnLines} and rtl=${rtlEnabled}(T1318812)`, async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const dataGrid = new DataGrid(GRID_CONTAINER); + + await t.expect(dataGrid.isReady()).ok(); + + await testScreenshot( + t, + takeScreenshot, + `T1318812__datagrid__band-columns__vertical-borders(showColumnLines=${showColumnLines},rtl=${rtlEnabled}).png`, + { element: dataGrid.element }, + ); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [ + { + Col1: 'Data A', Col2: 'Desc A', Col3: 'Group 1', Col4: 'X', Col5: 100, Col6: 50, + }, + { + Col1: 'Data B', Col2: 'Desc B', Col3: 'Group 1', Col4: 'Y', Col5: 200, Col6: 20, + }, + { + Col1: 'Data C', Col2: 'Desc C', Col3: 'Group 2', Col4: 'Z', Col5: 300, Col6: 10, + }, + ], + columns: [ + 'Col1', + { + caption: 'Band Column 1', + columns: [ + { + caption: 'Nested BandColumn 1', + columns: [ + { dataField: 'Col2', width: 300 }, + { dataField: 'Col3', width: 300 }, + ], + }, + ], + }, + { + caption: 'Band Column 2', + columns: [ + { + caption: 'Nested Band Column 2', + columns: [ + { dataField: 'Col4', width: 120 }, + ], + }, + ], + }, + { + caption: 'Band Column 3', + columns: [ + { + caption: 'Nested Band Column 3', + columns: [ + { dataField: 'Col5', width: 150 }, + { dataField: 'Col6', width: 150 }, + ], + }, + ], + }, + ], + showColumnLines, + rtlEnabled, + columnWidth: 100, + }); + }); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/runtimeChange.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/visual.ts similarity index 57% rename from e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/runtimeChange.ts rename to e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/visual.ts index a3c1a686041c..bcdbac329620 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/runtimeChange.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/visual.ts @@ -1,12 +1,11 @@ import { ClientFunction } from 'testcafe'; import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; -import Button from 'devextreme-testcafe-models/button'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; import url from '../../../../helpers/getPageUrl'; import { createWidget } from '../../../../helpers/createWidget'; import { testScreenshot } from '../../../../helpers/themeUtils'; -fixture.disablePageReloads`Band columns: runtime change` +fixture.disablePageReloads`Band columns.Visual` .page(url(__dirname, '../../../container.html')); const GRID_CONTAINER = '#container'; @@ -114,71 +113,3 @@ test('Should change usual columns to band columns without error in React (T12136 showBorders: true, }); }); - -test('Changing dataField for a banded column with the columnOption method does not work as expected (T1210340)', async (t) => { - const dataGrid = new DataGrid(GRID_CONTAINER); - const changeFieldButton = new Button('#otherContainer'); - - await t - .expect(dataGrid.getDataCell(0, 4).element.innerText) - .eql('2353025') - .click(changeFieldButton.element) - .expect(dataGrid.getDataCell(0, 4).element.innerText) - .eql('0.672'); -}).before(async () => { - await createWidget('dxDataGrid', { - dataSource: [{ - id: 1, - Country: 'Brazil', - Area: 8515767, - Population_Urban: 0.85, - Population_Rural: 0.15, - Population_Total: 205809000, - GDP_Agriculture: 0.054, - GDP_Industry: 0.274, - GDP_Services: 0.672, - GDP_Total: 2353025, - }], - columns: [ - 'Country', - 'Area', { - caption: 'Population', - columns: [ - 'Population_Total', - 'Population_Urban', - ], - }, { - caption: 'Nominal GDP', - columns: [{ - caption: 'Total, mln $', - dataField: 'GDP_Total', - name: 'GDP_Total', - }, { - caption: 'By Sector', - columns: [{ - caption: 'Agriculture', - dataField: 'GDP_Agriculture', - }, { - caption: 'Industry', - dataField: 'GDP_Industry', - format: { - type: 'percent', - }, - }, { - caption: 'Services', - dataField: 'GDP_Services', - }], - }], - }], - keyExpr: 'id', - showBorders: true, - }); - - await createWidget('dxButton', { - text: 'Change fields', - onClick() { - const grid = ($('#container') as any).dxDataGrid('instance'); - grid.columnOption('GDP_Total', 'dataField', 'GDP_Services'); - }, - }, '#otherContainer'); -}); diff --git a/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss b/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss index a6c246b5cf81..933f3674eb0a 100644 --- a/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss @@ -56,7 +56,7 @@ $datagrid-focused-border-color: null !default; border-right-color: $datagrid-border-color; } -// (0,4,1) +// (0,4,1) TODO: remove redundant rules .dx-header-multi-row.dx-datagrid-sticky-columns .dx-column-lines > td:first-child { border-left: $datagrid-border; border-left-color: $datagrid-border-color; @@ -97,13 +97,13 @@ $datagrid-focused-border-color: null !default; border-left-color: $datagrid-border-color; } -// (0,6,0) -.dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row .dx-datagrid-column-no-border { +// (0,6,1) +.dx-widget:not(.dx-rtl) .dx-datagrid-content .dx-datagrid-table .dx-row > td.dx-datagrid-first-header { border-left: none; } -// (0,7,1) -.dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row.dx-column-lines > td.dx-datagrid-first-header { +// (0,7,0) +.dx-widget:not(.dx-rtl) .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row .dx-datagrid-column-no-border { border-left: none; } @@ -192,28 +192,16 @@ $datagrid-focused-border-color: null !default; border-left-color: $datagrid-border-color; } -// (0,7,0) -.dx-rtl .dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row .dx-datagrid-column-no-border { +// (0,6,1) +.dx-rtl .dx-datagrid .dx-datagrid-content .dx-datagrid-table .dx-row > td.dx-datagrid-first-header { border-right: none; } -// (0,8,0) -.dx-rtl .dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row .dx-datagrid-column-no-border.dx-datagrid-sticky-column-border-left { - border-left: 2px solid; - border-left-color: $datagrid-border-color; -} - -// (0,8,1) -.dx-rtl .dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row.dx-column-lines > td.dx-datagrid-first-header { +// (0,7,0) +.dx-rtl .dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row .dx-datagrid-column-no-border { border-right: none; } -// (0,9,1) -.dx-rtl .dx-datagrid .dx-datagrid-sticky-columns .dx-datagrid-content .dx-datagrid-table .dx-row.dx-column-lines > td.dx-datagrid-first-header.dx-datagrid-sticky-column-border-left { - border-left: 2px solid; - border-left-color: $datagrid-border-color; -} - // #endregion // #region padding diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/cell.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/cell.scss index ef68d50fe188..fe124d01805f 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/cell.scss @@ -190,8 +190,8 @@ } // (0,4,2) - .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row > td.dx-#{$widget-name}-group-space + td, - .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row > tr > td.dx-#{$widget-name}-group-space + td { + .dx-widget:not(.dx-rtl) .dx-row > td.dx-#{$widget-name}-group-space + td, + .dx-widget:not(.dx-rtl) .dx-row > tr > td.dx-#{$widget-name}-group-space + td { border-left: none; } diff --git a/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss b/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss index 257dc34b4166..f4774da86241 100644 --- a/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss @@ -80,13 +80,13 @@ $treelist-focused-border-color: null !default; border-left-color: $treelist-border-color; } -// (0,6,0) -.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row .dx-treelist-column-no-border { +// (0,6,1) +.dx-treelist:not(.dx-rtl) .dx-treelist-content .dx-treelist-table .dx-row > td.dx-treelist-first-header { border-left: none; } -// (0,7,1) -.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row.dx-column-lines > td.dx-treelist-first-header { +// (0,7,0) +.dx-treelist:not(.dx-rtl) .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row .dx-treelist-column-no-border { border-left: none; } @@ -153,28 +153,16 @@ $treelist-focused-border-color: null !default; border-left: none; } -// (0,7,0) -.dx-rtl.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row .dx-treelist-column-no-border { +// (0,6,1) +.dx-rtl.dx-treelist .dx-treelist-content .dx-treelist-table .dx-row > td.dx-treelist-first-header { border-right: none; } -// (0,8,0) -.dx-rtl.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row .dx-treelist-column-no-border.dx-treelist-sticky-column-border-left { - border-left: 2px solid; - border-left-color: $treelist-border-color; -} - -// (0,8,1) -.dx-rtl.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row.dx-column-lines > td.dx-treelist-first-header { +// (0,7,0) +.dx-rtl.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row .dx-treelist-column-no-border { border-right: none; } -// (0,9,1) -.dx-rtl.dx-treelist .dx-treelist-sticky-columns .dx-treelist-content .dx-treelist-table .dx-row.dx-column-lines > td.dx-treelist-first-header.dx-treelist-sticky-column-border-left { - border-left: 2px solid; - border-left-color: $treelist-border-color; -} - // #endregion // #region padding diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss index 6a3c05a0fb08..8ce6164282d0 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss @@ -128,12 +128,6 @@ border-bottom-color: $datagrid-row-focused-bg; } - // (0,4,1) - .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row.dx-header-row > td { - border-right: 1px solid; - border-right-color: $fluent-grid-base-border-color; - } - // (0,4,1) .dx-#{$widget-name}-rowsview .dx-row.dx-edit-row:first-child > td { border-top-width: 0; @@ -141,6 +135,12 @@ border-bottom-color: $datagrid-border-color; } + // (0,4,1) + .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { + border-left: 1px solid; + border-left-color: $datagrid-border-color; + } + // (0,4,1) - (0,5,1) .dx-#{$widget-name}-rowsview.dx-#{$widget-name}-sticky-columns .dx-row-removed.dx-row-lines > td, .dx-#{$widget-name}-rowsview.dx-#{$widget-name}-sticky-columns .dx-row.dx-row-lines.dx-edit-row > td { @@ -179,22 +179,6 @@ border-bottom-color: $datagrid-row-selected-border-color; } - // (0,7,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td { - border-left: 1px solid; - border-left-color: $fluent-grid-base-border-color; - } - - // (0,8,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td:first-child { - border-left: none; - } - - // (0,8,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td:last-child { - border-right: none; - } - // (0,10,1) .dx-rtl .dx-data-row.dx-state-hover:not(.dx-selection):not(.dx-row-inserted):not(.dx-row-removed):not(.dx-edit-row):not(.dx-row-focused) > td:not(.dx-focused).dx-#{$widget-name}-group-space { border-left-color: $datagrid-hover-bg; @@ -211,6 +195,14 @@ border-right-color: $datagrid-row-selected-border-color; } + // (0,5,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { + border-left: none; + border-right: 1px solid; + border-right-color: $datagrid-border-color; + } + + // (0,5,1) - (0,6,2) .dx-rtl .dx-#{$widget-name}-rowsview .dx-selection.dx-row > td.dx-pointer-events-none, .dx-rtl .dx-#{$widget-name}-rowsview .dx-selection.dx-row > tr > td.dx-pointer-events-none, diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss index 3a7eb9169271..7a36d92a11a9 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss @@ -153,6 +153,12 @@ border-right-color: $datagrid-selection-bg; } + // (0,4,1) + .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { + border-left: 1px solid; + border-left-color: $datagrid-border-color; + } + // (0,4,1) - (0,4,2) .dx-#{$widget-name}-rowsview .dx-selection.dx-row:not(.dx-row-focused) > td.dx-pointer-events-none, .dx-#{$widget-name}-rowsview .dx-selection.dx-row:not(.dx-row-focused) > tr > td.dx-pointer-events-none, @@ -278,6 +284,13 @@ border-left-color: $datagrid-row-selected-border-color; } + // (0,5,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { + border-left: none; + border-right: 1px solid; + border-right-color: $datagrid-border-color; + } + // #endregion // #region padding diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss index aa44044d4cf5..0ba45b3d12c8 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss @@ -164,26 +164,10 @@ border-bottom-color: $datagrid-row-selected-border-color; } - // (0,5,1) - .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row.dx-header-row > td { - border-right: 1px solid; - border-right-color: $material-grid-base-border-color; - } - - // (0,7,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td { + // (0,4,1) + .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { border-left: 1px solid; - border-left-color: $material-grid-base-border-color; - } - - // (0,8,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td:first-child { - border-left: none; - } - - // (0,8,1) - .dx-#{$widget-name}-headers.dx-header-multi-row:not(.dx-#{$widget-name}-sticky-columns) .dx-#{$widget-name}-content .dx-#{$widget-name}-table .dx-row.dx-header-row > td:last-child { - border-right: none; + border-left-color: $datagrid-border-color; } // sticky-columns border @@ -235,6 +219,13 @@ border-left-color: $datagrid-row-selected-border-color; } + // (0,5,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-header-row:not(.dx-column-lines) > td { + border-left: none; + border-right: 1px solid; + border-right-color: $datagrid-border-color; + } + // #endregion // #region padding diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts index dd6dd918304d..4d4c8cc4131b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts @@ -49,12 +49,19 @@ export abstract class GridCoreModel { return this.root.querySelector(`.${SELECTORS.aiPromptEditor}`) as HTMLElement; } - public getHeaderCells(): NodeListOf { - return this.root.querySelectorAll(`.${SELECTORS.headerRowClass} > td`); + public getHeaderRows(): NodeListOf { + return this.root.querySelectorAll(`.${SELECTORS.headerRowClass}`); } - public getHeaderCell(columnIndex: number): HeaderCellModel { - return new HeaderCellModel(this.getHeaderCells()[columnIndex], this.addWidgetPrefix.bind(this)); + public getHeaderCells(rowIndex = 0): NodeListOf { + return this.getHeaderRows()[rowIndex].querySelectorAll('td'); + } + + public getHeaderCell(rowIndex = 0, columnIndex = 0): HeaderCellModel { + return new HeaderCellModel( + this.getHeaderCells(rowIndex)[columnIndex], + this.addWidgetPrefix.bind(this), + ); } public getAIHeaderCell(columnIndex: number): AIHeaderCellModel { diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/__tests__/m_column_headers.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/__tests__/m_column_headers.test.ts deleted file mode 100644 index 42e87ba55f16..000000000000 --- a/packages/devextreme/js/__internal/grids/grid_core/column_headers/__tests__/m_column_headers.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import $ from '@js/core/renderer'; - -import { - simulateHoverEvent, - simulateTextOverflow, -} from '../../__tests__/__mock__/helpers/dom_utils'; -import { - afterTest, - beforeTest, - createDataGrid, -} from '../../__tests__/__mock__/helpers/utils'; - -describe('Column Headers', () => { - beforeEach(beforeTest); - afterEach(afterTest); - - describe('headerCellTemplate', () => { - it('should apply right alignment to number column when headerCellTemplate is used', async () => { - const { component } = await createDataGrid({ - dataSource: [], - showBorders: true, - headerFilter: { - visible: true, - }, - columns: [ - { - dataField: 'test', - dataType: 'number', - headerCellTemplate(headerElement) { - $('') - .text('Test') - .appendTo(headerElement); - }, - }, - ], - }); - expect(component.getHeaderCell(0).getAlignment()).toBe('right'); - }); - }); - - describe('when cellHintEnabled: true', () => { - it('should show column caption in the tooltip instead of sort index when hovering sort indicator (T1321834)', async () => { - const { component } = await createDataGrid({ - dataSource: [{ id: 1, Position: 'Developer', Name: 'John' }], - columns: [ - { - dataField: 'Position', - caption: 'Position', - width: 30, - sortOrder: 'asc', - sortIndex: 0, - }, - { - dataField: 'Name', - caption: 'Name', - sortOrder: 'desc', - sortIndex: 1, - }, - ], - sorting: { - mode: 'multiple', - showSortIndexes: true, - }, - cellHintEnabled: true, - }); - - const headerCellElement = component.getHeaderCells()[0]; - const headerContentElement = component.getHeaderCell(0).getHeaderContent() as HTMLElement; - - simulateTextOverflow(headerContentElement, 50, 20); - simulateHoverEvent(headerCellElement); - - expect($(headerCellElement).attr('title')).toBe('Position'); - }); - - it('should show cell text in the tooltip for non-header rows', async () => { - const { instance } = await createDataGrid({ - dataSource: [{ id: 1, Position: 'Very Long Position Name That Should Be Truncated' }], - columns: [ - { - dataField: 'Position', - caption: 'Position', - width: 50, - }, - ], - cellHintEnabled: true, - }); - - const dataCell = instance.getCellElement(0, 0) as HTMLElement; - - simulateTextOverflow(dataCell, 200, 50); - simulateHoverEvent(dataCell); - - expect($(dataCell).attr('title')).toBe(dataCell.textContent); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts index 5bc102d854f5..7b21f94c7b85 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts @@ -1,3 +1,4 @@ export const CLASSES = { cellContent: 'text-content', + firstHeader: 'first-header', }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.integration.test.ts new file mode 100644 index 000000000000..8ac7d46c50a1 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.integration.test.ts @@ -0,0 +1,196 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import { + simulateHoverEvent, + simulateTextOverflow, +} from '../__tests__/__mock__/helpers/dom_utils'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '../__tests__/__mock__/helpers/utils'; + +describe('Column Headers', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('headerCellTemplate', () => { + it('should apply right alignment to number column when headerCellTemplate is used', async () => { + const { component } = await createDataGrid({ + dataSource: [], + showBorders: true, + headerFilter: { + visible: true, + }, + columns: [ + { + dataField: 'test', + dataType: 'number', + headerCellTemplate(headerElement) { + $('') + .text('Test') + .appendTo(headerElement); + }, + }, + ], + }); + expect(component.getHeaderCell(0).getAlignment()).toBe('right'); + }); + }); + + describe('when cellHintEnabled: true', () => { + it('should show column caption in the tooltip instead of sort index when hovering sort indicator (T1321834)', async () => { + const { component } = await createDataGrid({ + dataSource: [{ id: 1, Position: 'Developer', Name: 'John' }], + columns: [ + { + dataField: 'Position', + caption: 'Position', + width: 30, + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'Name', + caption: 'Name', + sortOrder: 'desc', + sortIndex: 1, + }, + ], + sorting: { + mode: 'multiple', + showSortIndexes: true, + }, + cellHintEnabled: true, + }); + + const headerCellElement = component.getHeaderCells()[0]; + const headerContentElement = component.getHeaderCell(0).getHeaderContent() as HTMLElement; + + simulateTextOverflow(headerContentElement, 50, 20); + simulateHoverEvent(headerCellElement); + + expect($(headerCellElement).attr('title')).toBe('Position'); + }); + + it('should show cell text in the tooltip for non-header rows', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ id: 1, Position: 'Very Long Position Name That Should Be Truncated' }], + columns: [ + { + dataField: 'Position', + caption: 'Position', + width: 50, + }, + ], + cellHintEnabled: true, + }); + + const dataCell = instance.getCellElement(0, 0) as HTMLElement; + + simulateTextOverflow(dataCell, 200, 50); + simulateHoverEvent(dataCell); + + expect($(dataCell).attr('title')).toBe(dataCell.textContent); + }); + }); + + describe('toggleFirstHeaderClass', () => { + it('should add first-header class to the first column', async () => { + const { component } = await createDataGrid({ + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + 'field1', + { + caption: 'Band', + columns: ['field2', 'field3'], + }, + ], + }); + + const $headerCell = $(component.getHeaderCell(0, 0).getElement()); + expect($headerCell.hasClass('dx-datagrid-first-header')).toBe(true); + }); + + it('should not add first-header class to non-first columns', async () => { + const { component } = await createDataGrid({ + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + 'field1', + { + caption: 'Band', + columns: ['field2', 'field3'], + }, + ], + }); + + const $secondCellOfFirstRow = $(component.getHeaderCell(0, 1).getElement()); + const $firstCellOfSecondRow = $(component.getHeaderCell(1, 0).getElement()); + const $secondCellOfSecondRow = $(component.getHeaderCell(1, 1).getElement()); + + expect($secondCellOfFirstRow.hasClass('dx-datagrid-first-header')).toBe(false); + expect($firstCellOfSecondRow.hasClass('dx-datagrid-first-header')).toBe(false); + expect($secondCellOfSecondRow.hasClass('dx-datagrid-first-header')).toBe(false); + }); + + it('should update first-header class when first column visibility changes', async () => { + const { component } = await createDataGrid({ + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + 'field1', + { + caption: 'Band', + columns: ['field2', 'field3'], + }, + ], + }); + + component.apiColumnOption('field1', 'visible', false); + + const $firstHeaderCell = $(component.getHeaderCell(0, 0).getElement()); + expect($firstHeaderCell.text()).toBe('Band'); + expect($firstHeaderCell.hasClass('dx-datagrid-first-header')).toBe(true); + }); + + it('should add first-header class when band column is first', async () => { + const { component } = await createDataGrid({ + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + { + caption: 'Band', + columns: ['field1', 'field2'], + }, + 'field3', + ], + }); + + const $firstCellOfFirstRow = $(component.getHeaderCell(0, 0).getElement()); + const $firstCellOfSecondRow = $(component.getHeaderCell(1, 0).getElement()); + + expect($firstCellOfFirstRow.hasClass('dx-datagrid-first-header')).toBe(true); + expect($firstCellOfSecondRow.hasClass('dx-datagrid-first-header')).toBe(true); + }); + + it('should not add first-header class to non-first columns when band column is first', async () => { + const { component } = await createDataGrid({ + dataSource: [{ field1: 1, field2: 2, field3: 3 }], + columns: [ + { + caption: 'Band', + columns: ['field1', 'field2'], + }, + 'field3', + ], + }); + + const $secondCellOfFirstRow = $(component.getHeaderCell(0, 1).getElement()); + const $secondCellOfSecondRow = $(component.getHeaderCell(1, 1).getElement()); + + expect($secondCellOfFirstRow.hasClass('dx-datagrid-first-header')).toBe(false); + expect($secondCellOfSecondRow.hasClass('dx-datagrid-first-header')).toBe(false); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts index 71174c016b90..e3f447f0e481 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts @@ -77,6 +77,36 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { .toggleClass(HEADER_FILTER_INDICATOR_CLASS, !!$visibleIndicatorElements.filter(`.${this._getIndicatorClassName('headerFilter')}`).length); } + private toggleFirstHeaderClass( + $cell: dxElementWrapper, + column: Column, + rowIndex: number | null, + ): void { + const columnsController = this._columnsController; + const isFirstColumn = columnsController?.isFirstColumn(column, rowIndex); + + $cell.toggleClass(this.addWidgetPrefix(CLASSES.firstHeader), isFirstColumn); + } + + private updateFirstHeaderClasses(): void { + const $rows = this._getRowElementsCore().toArray(); + + $rows.forEach((row: Element, index: number) => { + const rowIndex = index; + const $cells = $(row).children('td').toArray(); + const columns = this.getColumns(rowIndex); + + $cells.forEach((cell: Element, cellIndex: number) => { + const $cell = $(cell); + const column = columns[cellIndex]; + + if (column) { + this.toggleFirstHeaderClass($cell, column, rowIndex); + } + }); + }); + } + protected createCellContent( $cell: dxElementWrapper, column: Column, @@ -359,8 +389,15 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { const { column } = options; // @ts-expect-error const $cellElement = super._createCell.apply(this, arguments); + const rowCount = this.getRowCount(); + + if (rowCount > 1) { + this.toggleFirstHeaderClass($cellElement, column, options.rowIndex); + } - column.rowspan > 1 && options.rowType === 'header' && $cellElement.attr('rowSpan', column.rowspan); + if (column.rowspan > 1 && options.rowType === 'header') { + $cellElement.attr('rowSpan', column.rowspan); + } return $cellElement; } @@ -459,6 +496,17 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { return returnAll ? $indicatorsContainer : $indicatorsContainer.filter(`:not(.${VISIBILITY_HIDDEN_CLASS})`); } + protected _resizeCore(): void { + const rowCount = this.getRowCount(); + const columnHidingEnabled = this.option('columnHidingEnabled'); + + super._resizeCore.apply(this); + + if (rowCount > 1 && columnHidingEnabled) { + this.updateFirstHeaderClasses(); + } + } + /** * @extended: tree_list/selection */ diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.integration.test.ts new file mode 100644 index 000000000000..234ef1a94e38 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.integration.test.ts @@ -0,0 +1,65 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import type { DataGridCommandColumnType } from '@js/ui/data_grid'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from '../__tests__/__mock__/helpers/utils'; + +const UNSUPPORTED_GROUPING_COLUMN_TYPES = ['adaptive', 'buttons', 'detailExpand', 'groupExpand', 'selection', 'drag', 'ai']; + +const dataSource = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, +]; + +describe('Column Controller', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('Grouping for unsupported column types', () => { + describe.each(UNSUPPORTED_GROUPING_COLUMN_TYPES)('unsupported grouping column types', (columnType) => { + it(`Should have no group rows after put type property = ${columnType} (first load)`, async () => { + const { component } = await createDataGrid({ + dataSource, + showBorders: true, + columns: [ + 'id', + { + caption: 'Test', + type: columnType as DataGridCommandColumnType | undefined, + name: 'test', + groupIndex: 0, + }, + ], + }); + + const groupRow = component.getGroupRows(); + expect(groupRow.length).toBe(0); + }); + + it(`Should have no group rows after put type property = ${columnType} (dynamic update)`, async () => { + const { component } = await createDataGrid({ + dataSource, + showBorders: true, + columns: [ + 'id', + { + caption: 'Test', + name: 'AItest', + }, + ], + }); + + component.apiColumnOption('AItest', 'type', columnType as DataGridCommandColumnType | undefined); + + const groupRow = component.getGroupRows(); + expect(groupRow.length).toBe(0); + }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts index 234ef1a94e38..daeafa8c664d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts @@ -1,65 +1,114 @@ import { - afterEach, beforeEach, describe, expect, it, + describe, expect, it, jest, } from '@jest/globals'; -import type { DataGridCommandColumnType } from '@js/ui/data_grid'; -import { - afterTest, - beforeTest, - createDataGrid, -} from '../__tests__/__mock__/helpers/utils'; +import type { ColumnsController } from './m_columns_controller'; +import { ColumnsController as ColumnsControllerClass } from './m_columns_controller'; + +interface ControllerConfig { + columns?: object[]; + options?: Record; +} -const UNSUPPORTED_GROUPING_COLUMN_TYPES = ['adaptive', 'buttons', 'detailExpand', 'groupExpand', 'selection', 'drag', 'ai']; +interface ComponentMock { + option: jest.Mock<(optionName: string) => unknown>; + _createComponent: jest.Mock; + element: jest.Mock<() => { jquery: boolean }>; + _controllers: { + data: { getDataSource: jest.Mock<() => null> }; + focus: Record; + stateStoring: Record; + }; +} -const dataSource = [ - { id: 1, name: 'Item 1' }, - { id: 2, name: 'Item 2' }, - { id: 3, name: 'Item 3' }, -]; +const createRealColumnsController = (config: ControllerConfig): ColumnsController => { + const componentMock: ComponentMock = { + option: jest.fn((optionName: string) => { + if (optionName === 'columns') return config.columns ?? []; + return config.options?.[optionName]; + }), + _createComponent: jest.fn(), + element: jest.fn(() => ({ + jquery: true, + })), + _controllers: { + data: { getDataSource: jest.fn(() => null) }, + focus: {}, + stateStoring: {}, + }, + }; -describe('Column Controller', () => { - beforeEach(beforeTest); - afterEach(afterTest); + const controller = new ColumnsControllerClass(componentMock); + controller.init(); - describe('Grouping for unsupported column types', () => { - describe.each(UNSUPPORTED_GROUPING_COLUMN_TYPES)('unsupported grouping column types', (columnType) => { - it(`Should have no group rows after put type property = ${columnType} (first load)`, async () => { - const { component } = await createDataGrid({ - dataSource, - showBorders: true, + return controller; +}; + +describe('Methods', () => { + describe('getVisibleDataColumnsByBandColumn', () => { + it('should return only visible data columns', () => { + const columns = [ + { + caption: 'Band', columns: [ - 'id', - { - caption: 'Test', - type: columnType as DataGridCommandColumnType | undefined, - name: 'test', - groupIndex: 0, - }, + { dataField: 'field1' }, + { dataField: 'field2', visible: false }, + { dataField: 'field3' }, ], - }); + }, + ]; + + const controller = createRealColumnsController({ columns }); + const bandColumn = controller.getColumns()[0]; + const result = controller.getVisibleDataColumnsByBandColumn(bandColumn.index); - const groupRow = component.getGroupRows(); - expect(groupRow.length).toBe(0); - }); + expect(result).toHaveLength(2); + expect(result.map((c) => c.dataField as string)).toEqual(['field1', 'field3']); + }); - it(`Should have no group rows after put type property = ${columnType} (dynamic update)`, async () => { - const { component } = await createDataGrid({ - dataSource, - showBorders: true, + it('should recursively get data columns from nested bands', () => { + const columns = [ + { + caption: 'Band 1', columns: [ - 'id', + { dataField: 'field1' }, { - caption: 'Test', - name: 'AItest', + caption: 'Band 2', + columns: [ + { dataField: 'field2' }, + { dataField: 'field3' }, + ], }, ], - }); + }, + ]; + + const controller = createRealColumnsController({ columns }); + const bandColumn = controller.getColumns()[0]; + const result = controller.getVisibleDataColumnsByBandColumn(bandColumn.index); + + expect(result).toHaveLength(3); + expect(result.map((c) => c.dataField as string)).toEqual(['field1', 'field2', 'field3']); + }); + + it('should exclude grouped columns without showWhenGrouped', () => { + const columns = [ + { + caption: 'Band', + columns: [ + { dataField: 'field1', groupIndex: 0 }, + { dataField: 'field2', groupIndex: 0, showWhenGrouped: true }, + { dataField: 'field3' }, + ], + }, + ]; - component.apiColumnOption('AItest', 'type', columnType as DataGridCommandColumnType | undefined); + const controller = createRealColumnsController({ columns }); + const bandColumn = controller.getColumns()[0]; + const result = controller.getVisibleDataColumnsByBandColumn(bandColumn.index); - const groupRow = component.getGroupRows(); - expect(groupRow.length).toBe(0); - }); + expect(result).toHaveLength(2); + expect(result.map((c) => c.dataField as string)).toEqual(['field2', 'field3']); }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index e6811452bf48..5e2f822a04a6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -1884,7 +1884,7 @@ export class ColumnsController extends modules.Controller { public getVisibleDataColumnsByBandColumn(bandColumnIndex: number) { const that = this; const bandColumnsCache = that.getBandColumnsCache(); - const result = this.getChildrenByBandColumn(bandColumnIndex, bandColumnsCache.columnChildrenByIndex); + const result = getChildrenByBandColumn(bandColumnIndex, bandColumnsCache.columnChildrenByIndex, true); return result .filter((column) => !column.isBand && column.visible); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts index 1f0267472113..54056cdb7e0e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts @@ -1083,7 +1083,7 @@ export const isFirstOrLastColumn = function ( ): boolean { const targetColumnIndex = targetColumn.index; const bandColumnsCache = that.getBandColumnsCache(); - const parentBandColumns = !isDefined(targetColumn.type) && getParentBandColumns(targetColumnIndex, bandColumnsCache.columnParentByIndex); + const parentBandColumns = getParentBandColumns(targetColumnIndex, bandColumnsCache.columnParentByIndex); if (parentBandColumns?.length) { return isFirstOrLastBandColumn(that, parentBandColumns.concat([targetColumn]), onlyWithinBandColumn, isLast, fixedPosition); 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 a983617b08b6..553970e1445a 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 @@ -13,7 +13,6 @@ export const CLASSES = { stickyColumnBorderRight: 'sticky-column-border-right', stickyColumnBorderLeft: 'sticky-column-border-left', stickyColumns: 'sticky-columns', - firstHeader: 'first-header', columnNoBorder: 'column-no-border', groupRowContainer: 'group-row-container', focusedFixedElement: 'dx-focused-fixed-element', 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 0cb8682a0f38..f9ac8efe584e 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 @@ -29,10 +29,6 @@ const addStickyColumnClass = ($cell, fixedPosition, addWidgetPrefix): void => { } }; -const toggleFirstHeaderClass = ($cell, value, addWidgetPrefix): void => { - $cell.toggleClass(addWidgetPrefix(CLASSES.firstHeader), value); -}; - const toggleColumnNoBorderClass = ($cell, value, addWidgetPrefix): void => { $cell.toggleClass(addWidgetPrefix(CLASSES.columnNoBorder), value); }; @@ -349,7 +345,6 @@ const getNextHeaderCell = ( }; export const GridCoreStickyColumnsDom = { - toggleFirstHeaderClass, toggleColumnNoBorderClass, addStickyColumnClass, addStickyColumnBorderLeftClass, 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 2dc035895c87..009ed0958c8d 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 @@ -9,6 +9,7 @@ import type { ResizingController } from '@ts/grids/grid_core/views/m_grid_view'; import { HIDDEN_COLUMNS_WIDTH } from '../adaptivity/const'; import type { ColumnHeadersView } from '../column_headers/m_column_headers'; +import type { Column } from '../columns_controller/m_columns_controller'; import type { ColumnsResizerViewController, DraggingHeaderViewController, @@ -83,9 +84,9 @@ const baseStickyColumns = >(Base: T) => class } } - private updateBorderCellClasses( + private toggleColumnNoBorderClass( $cell: dxElementWrapper, - column, + column: Column, rowIndex: number | null, ): void { const columnsController = this._columnsController; @@ -96,15 +97,12 @@ const baseStickyColumns = >(Base: T) => class rowIndex, isRowsView, ); - const isFirstColumn = columnsController?.isFirstColumn(column, rowIndex); GridCoreStickyColumnsDom .toggleColumnNoBorderClass($cell, needToRemoveBorder, this.addWidgetPrefix.bind(this)); - GridCoreStickyColumnsDom - .toggleFirstHeaderClass($cell, isFirstColumn, this.addWidgetPrefix.bind(this)); } - private _updateBorderClasses(): void { + private updateColumnNoBorderClasses(): void { const isColumnHeadersView = this.name === 'columnHeadersView'; const $rows = this._getRowElementsCore().not(`.${MASTER_DETAIL_CLASSES.detailRow}`).toArray(); @@ -120,7 +118,7 @@ const baseStickyColumns = >(Base: T) => class const column = columns[cellIndex]; if (column.visibleWidth !== HIDDEN_COLUMNS_WIDTH) { - this.updateBorderCellClasses($cell, column, rowIndex); + this.toggleColumnNoBorderClass($cell, column, rowIndex); } }); }); @@ -156,7 +154,7 @@ const baseStickyColumns = >(Base: T) => class const isExpandColumn = column.command && column.command === 'expand'; if (hasStickyColumns && !needToDisableStickyColumn(this._columnsController, column)) { - this.updateBorderCellClasses($cell, column, rowIndex); + this.toggleColumnNoBorderClass($cell, column, rowIndex); if (column.fixed) { const fixedPosition = getColumnFixedPosition(this._columnsController, column); @@ -243,19 +241,20 @@ const baseStickyColumns = >(Base: T) => class } } - protected _resizeCore() { + protected _resizeCore(): void { const hasStickyColumns = this.hasStickyColumns(); - const adaptiveColumns = this.getController('adaptiveColumns'); - const hidingColumnsQueue = adaptiveColumns?.getHidingColumnsQueue(); + const columnHidingEnabled = this.option('columnHidingEnabled'); - super._resizeCore.apply(this, arguments as any); + super._resizeCore.apply(this); - if (hasStickyColumns) { - this.setStickyOffsets(); + if (!hasStickyColumns) { + return; + } - if (hidingColumnsQueue?.length) { - this._updateBorderClasses(); - } + this.setStickyOffsets(); + + if (columnHidingEnabled) { + this.updateColumnNoBorderClasses(); } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.test.ts new file mode 100644 index 000000000000..eab0dff479ed --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.test.ts @@ -0,0 +1,108 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; + +import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import { ColumnsController as ColumnsControllerClass } from '../columns_controller/m_columns_controller'; +import { needToRemoveColumnBorder } from './utils'; + +interface ControllerConfig { + columns?: object[]; + options?: Record; +} + +interface ComponentMock { + option: jest.Mock<(optionName: string) => unknown>; + _createComponent: jest.Mock; + element: jest.Mock<() => { jquery: boolean }>; + _controllers: { + data: { getDataSource: jest.Mock<() => null> }; + focus: Record; + stateStoring: Record; + }; +} + +const createRealColumnsController = (config: ControllerConfig): ColumnsController => { + const componentMock: ComponentMock = { + option: jest.fn((optionName: string) => { + if (optionName === 'columns') { + return config.columns ?? []; + } + + return config.options?.[optionName]; + }), + _createComponent: jest.fn(), + element: jest.fn(() => ({ + jquery: true, + })), + _controllers: { + data: { getDataSource: jest.fn(() => null) }, + focus: {}, + stateStoring: {}, + }, + }; + + const controller = new ColumnsControllerClass(componentMock); + controller.init(); + + return controller; +}; + +describe('needToRemoveColumnBorder', () => { + it('should not check parent column for grouped column', () => { + const columns = [ + { + dataField: 'field1', + fixed: true, + }, + { + caption: 'Band Column', + columns: [{ + dataField: 'field2', + groupIndex: 0, + }, { + dataField: 'field3', + }], + }, + ]; + + const controller = createRealColumnsController({ columns }); + const groupExpandColumn = controller.getVisibleColumns()[0]; + + expect(groupExpandColumn.type).toBe('groupExpand'); + expect(needToRemoveColumnBorder(controller, groupExpandColumn, 0)) + .toBe(false); + }); + + it('should return false for grouped column with showWhenGrouped', () => { + const columns = [ + { + dataField: 'field1', + fixed: true, + }, + { + caption: 'Band Column', + columns: [{ + dataField: 'field2', + groupIndex: 0, + showWhenGrouped: true, + }, { + dataField: 'field3', + }], + }, + ]; + + const controller = createRealColumnsController({ columns }); + const visibleColumns = controller.getVisibleColumns(); + const groupExpandColumn = visibleColumns[0]; + const groupedColumn = visibleColumns[2]; + + expect(groupExpandColumn.type).toBe('groupExpand'); + expect(needToRemoveColumnBorder(controller, groupExpandColumn, 0)) + .toBe(false); + + expect(groupedColumn.dataField).toBe('field2'); + expect(needToRemoveColumnBorder(controller, groupedColumn, 0)) + .toBe(true); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.ts index f2628a544425..e63045d143ac 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/sticky_columns/utils.ts @@ -115,7 +115,7 @@ const getStickyOffsetCore = function ( const isFirstOrLastColumn = function ( that: ColumnsController, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + column, rowIndex: number, onlyWithinBandColumn = false, @@ -243,7 +243,7 @@ export const needToRemoveColumnBorder = function ( isDataColumn = false, ): boolean { const visibleColumns = that.getVisibleColumns(isDataColumn ? null : rowIndex); - const parentColumn = that.getParentColumn(column); + const parentColumn = !isDefined(column.type) && that.getParentColumn(column); if (parentColumn) { const isFirstColumn = that.isFirstColumn(column, rowIndex, true); diff --git a/packages/devextreme/testing/helpers/gridBaseMocks.js b/packages/devextreme/testing/helpers/gridBaseMocks.js index 1cd526f839b2..7479b2ed8e01 100644 --- a/packages/devextreme/testing/helpers/gridBaseMocks.js +++ b/packages/devextreme/testing/helpers/gridBaseMocks.js @@ -649,6 +649,10 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo return true; }, + isFirstColumn: function() { + return false; + }, + getFirstDataColumnIndex: function() { const visibleColumns = this.getVisibleColumns(); const visibleColumnsLength = visibleColumns.length; diff --git a/packages/testcafe-models/dataGrid/headers/cell.ts b/packages/testcafe-models/dataGrid/headers/cell.ts index 2ba6e9f6c867..da9839500839 100644 --- a/packages/testcafe-models/dataGrid/headers/cell.ts +++ b/packages/testcafe-models/dataGrid/headers/cell.ts @@ -14,6 +14,7 @@ const CLASS = { stickyLeft: 'dx-datagrid-sticky-column-left', stickyRight: 'dx-datagrid-sticky-column-right', aiHeaderButton: 'dx-command-ai-header-button', + firstHeader: 'dx-datagrid-first-header', }; const getStickyClassNames = (position: StickyPosition | undefined): string[] => { @@ -38,6 +39,8 @@ export default class HeaderCell { isHidden: Promise; + isFirstHeader: Promise; + isSticky(position?: StickyPosition | undefined): Promise { return ClientFunction((element, stickyClassNames) => { const elementClassList = element().classList; @@ -56,6 +59,7 @@ export default class HeaderCell { this.element = headerRow.find(`td[aria-colindex='${index + 1}']`); this.isFocused = this.element.focused; this.isHidden = this.element.hasClass(Widget.addClassPrefix(widgetName, CLASS.hiddenColumn)); + this.isFirstHeader = this.element.hasClass(CLASS.firstHeader); } getFilterIcon(): Selector {