From 0557280b1e93b10b75bed36b1b21809087a423cb Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 5 Mar 2026 13:57:25 +0400 Subject: [PATCH 1/5] Revert the fix for T1282595 --- .../scss/widgets/fluent/gridBase/layout/cell.scss | 6 ------ .../scss/widgets/material/gridBase/layout/cell.scss | 6 ------ 2 files changed, 12 deletions(-) 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..93c8dda8e6b5 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; 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..5e7fd72f6ef8 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss @@ -164,12 +164,6 @@ 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 { border-left: 1px solid; From e01c44ed56c004f3cad404fa83389d44c321d509 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 2 Feb 2026 13:44:54 +0400 Subject: [PATCH 2/5] separate TestCafe tests into functional and visual tests --- .../dataGrid/common/bandColumns/functional.ts | 77 +++++++++++++++++++ .../{runtimeChange.ts => visual.ts} | 71 +---------------- 2 files changed, 78 insertions(+), 70 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts rename e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/{runtimeChange.ts => visual.ts} (57%) 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..70eafa829c61 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts @@ -0,0 +1,77 @@ +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'); +}); 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'); -}); From 585c4b22da7e153200418129d90807e387a9038f Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 18 Mar 2026 02:01:05 +0400 Subject: [PATCH 3/5] Move first-header class logic from sticky_columns to column_headers --- .../dataGrid/common/bandColumns/functional.ts | 46 ++++ .../common/bandColumns/matrix.functional.ts | 187 +++++++++++++++++ .../widgets/base/dataGrid/layout/cell.scss | 26 ++- .../widgets/base/treeList/layout/cell.scss | 26 ++- .../widgets/fluent/gridBase/layout/cell.scss | 20 +- .../widgets/generic/gridBase/layout/cell.scss | 12 ++ .../material/gridBase/layout/cell.scss | 20 +- .../__tests__/__mock__/model/grid_core.ts | 15 +- .../__tests__/m_column_headers.test.ts | 100 --------- .../grids/grid_core/column_headers/const.ts | 1 + .../m_column_headers.integration.test.ts | 196 ++++++++++++++++++ .../column_headers/m_column_headers.ts | 49 ++++- .../grids/grid_core/sticky_columns/const.ts | 1 - .../grids/grid_core/sticky_columns/dom.ts | 5 - .../sticky_columns/m_sticky_columns.ts | 33 ++- .../testing/helpers/gridBaseMocks.js | 4 + .../testcafe-models/dataGrid/headers/cell.ts | 4 + 17 files changed, 573 insertions(+), 172 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts delete mode 100644 packages/devextreme/js/__internal/grids/grid_core/column_headers/__tests__/m_column_headers.test.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.integration.test.ts diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts index 70eafa829c61..2246ffd71108 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/functional.ts @@ -75,3 +75,49 @@ test('Changing dataField for a banded column with the columnOption method does n }, }, '#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.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts new file mode 100644 index 000000000000..ce1866a5993a --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts @@ -0,0 +1,187 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Band columns.Matrix` + .page(url(__dirname, '../../../container.html')); + +const GRID_CONTAINER = '#container'; + +// [borderLeftWidth, borderRightWidth] in pixels +type Borders = [left: number, right: number]; + +interface CellBorderExpectation { + columnIndex: number; + name: string; + expected: Borders; +} + +interface RowBorderExpectation { + rowIndex: number; + cells: CellBorderExpectation[]; +} + +async function checkHeaderCellBorders( + t: TestController, + dataGrid: DataGrid, + expectedRows: RowBorderExpectation[], +): Promise { + const headers = dataGrid.getHeaders(); + + for (let r = 0; r < expectedRows.length; r += 1) { + const { rowIndex, cells } = expectedRows[r]; + const headerRow = headers.getHeaderRow(rowIndex); + + for (let c = 0; c < cells.length; c += 1) { + const { columnIndex, name, expected: [leftWidth, rightWidth] } = cells[c]; + const { element } = headerRow.getHeaderCell(columnIndex); + + const borderLeft = await element.getStyleProperty('border-left-width'); + const borderRight = await element.getStyleProperty('border-right-width'); + + await t + .expect(parseInt(borderLeft, 10)).eql( + leftWidth, + `"${name}" (row: ${rowIndex}, col: ${columnIndex}): border-left-width`, + ) + .expect(parseInt(borderRight, 10)).eql( + rightWidth, + `"${name}" (row: ${rowIndex}, col: ${columnIndex}): border-right-width`, + ); + } + } +} + +const configs = [{ + showColumnLines: true, + rtlEnabled: false, +}, { + showColumnLines: false, + rtlEnabled: false, +}, { + showColumnLines: true, + rtlEnabled: true, +}, { + showColumnLines: false, + rtlEnabled: true, +}]; + +// Check vertical borders of band header cells +configs.forEach(( + { showColumnLines, rtlEnabled }: { showColumnLines: boolean; rtlEnabled: boolean; }, +): void => { + // Header layout: + // Row 0: | Band Column 1 (cols 0–2) | Band Column 2 (cols 3–5) | + // Row 1: | Nested BandColumn 1 (0–2) | Nested Band Column 2 (3–5) | + // Row 2: | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 | + test(`Two band columns with three levels (showColumnLines: ${showColumnLines}, rtl: ${rtlEnabled})`, async (t) => { + const dataGrid = new DataGrid(GRID_CONTAINER); + const getExpectedBorders = (): RowBorderExpectation[] => { + if (showColumnLines) { + return [ + { + rowIndex: 0, + cells: [ + { columnIndex: 0, name: 'Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 1] }, + { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, + ], + }, + { + rowIndex: 1, + cells: [ + { columnIndex: 0, name: 'Nested Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 1] }, + { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, + ], + }, + { + rowIndex: 2, + cells: [ + { columnIndex: 0, name: 'Col1', expected: rtlEnabled ? [1, 0] : [0, 1] }, + { columnIndex: 1, name: 'Col2', expected: [1, 1] }, + { columnIndex: 2, name: 'Col3', expected: [1, 1] }, + { columnIndex: 3, name: 'Col4', expected: [1, 1] }, + { columnIndex: 4, name: 'Col5', expected: [1, 1] }, + { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 1] : [1, 0] }, + ], + }, + ]; + } + + return [ + { + rowIndex: 0, + cells: [ + { columnIndex: 0, name: 'Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 0] }, + { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, + ], + }, + { + rowIndex: 1, + cells: [ + { columnIndex: 0, name: 'Nested Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 0] }, + { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, + ], + }, + { + rowIndex: 2, + cells: [ + { columnIndex: 0, name: 'Col1', expected: rtlEnabled ? [1, 0] : [0, 0] }, + { columnIndex: 1, name: 'Col2', expected: [1, 0] }, + { columnIndex: 2, name: 'Col3', expected: [1, 0] }, + { columnIndex: 3, name: 'Col4', expected: [1, 0] }, + { columnIndex: 4, name: 'Col5', expected: [1, 0] }, + { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 0] : [1, 0] }, + ], + }, + ]; + }; + + await t.expect(dataGrid.isReady()).ok(); + await checkHeaderCellBorders(t, dataGrid, getExpectedBorders()); + }).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' }, + { dataField: 'Col2' }, + { dataField: 'Col3' }, + ], + }, + ], + }, + { + caption: 'Band Column 2', + columns: [ + { + caption: 'Nested Band Column 2', + columns: [ + { dataField: 'Col4' }, + { dataField: 'Col5' }, + { dataField: 'Col6' }, + ], + }, + ], + }, + ], + showColumnLines, + rtlEnabled, + columnWidth: 100, + }); + }); +}); 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..c4937fa74bc8 100644 --- a/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/base/dataGrid/layout/cell.scss @@ -102,11 +102,17 @@ $datagrid-focused-border-color: null !default; 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,6,1) +.dx-widget:not(.dx-rtl) > .dx-gridbase-container > .dx-header-multi-row .dx-row > td.dx-datagrid-first-header { border-left: none; } +// (0,7,1) +.dx-widget:not(.dx-rtl) > .dx-gridbase-container > .dx-header-multi-row .dx-row > td.dx-datagrid-first-header.dx-datagrid-sticky-column-border-left { + border-left: 2px solid; + border-left-color: $datagrid-border-color; +} + // RTL // (0,4,1) @@ -197,19 +203,19 @@ $datagrid-focused-border-color: null !default; 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,7,1) +.dx-rtl > .dx-gridbase-container > .dx-header-multi-row .dx-datagrid-content .dx-datagrid-table .dx-row > td.dx-datagrid-first-header { + border-right: none; } // (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 { - border-right: none; +.dx-rtl > .dx-gridbase-container > .dx-header-multi-row .dx-datagrid-content .dx-datagrid-table .dx-row > td.dx-datagrid-first-header.dx-datagrid-sticky-column-border-left { + border-left: 2px solid; + border-left-color: $datagrid-border-color; } -// (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 { +// (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; } 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..f4897cd1b4e3 100644 --- a/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/base/treeList/layout/cell.scss @@ -85,11 +85,17 @@ $treelist-focused-border-color: null !default; 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,6,1) +.dx-treelist:not(.dx-rtl) > .dx-gridbase-container > .dx-header-multi-row .dx-row > td.dx-treelist-first-header { border-left: none; } +// (0,7,1) +.dx-treelist:not(.dx-rtl) > .dx-gridbase-container > .dx-header-multi-row .dx-row > td.dx-treelist-first-header.dx-treelist-sticky-column-border-left { + border-left: 2px solid; + border-left-color: $treelist-border-color; +} + // RTL // (0,4,1) @@ -158,19 +164,19 @@ $treelist-focused-border-color: null !default; 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,7,1) +.dx-rtl > .dx-gridbase-container > .dx-header-multi-row .dx-treelist-content .dx-treelist-table .dx-row > td.dx-treelist-first-header { + border-right: none; } // (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 { - border-right: none; +.dx-rtl > .dx-gridbase-container > .dx-header-multi-row .dx-treelist-content .dx-treelist-table .dx-row > td.dx-treelist-first-header.dx-treelist-sticky-column-border-left { + border-left: 2px solid; + border-left-color: $treelist-border-color; } -// (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 { +// (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; } 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 93c8dda8e6b5..4a57d06b06ff 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/cell.scss @@ -174,19 +174,9 @@ } // (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 { + .dx-widget:not(.dx-rtl) .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-left, .dx-#{$widget-name}-column-no-border) { 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; + border-left-color: $datagrid-border-color; } // (0,10,1) @@ -213,6 +203,12 @@ border-left-color: $datagrid-row-selected-border-color; } + // (0,6,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-right, .dx-#{$widget-name}-column-no-border) { + border-right: 1px solid; + border-right-color: $datagrid-border-color; + } + // (0,6,1) - (0,7,2) .dx-rtl .dx-#{$widget-name}-rowsview .dx-selection.dx-row > td:not(.dx-focused).dx-#{$widget-name}-group-space, .dx-rtl .dx-#{$widget-name}-rowsview .dx-selection.dx-row > tr > td:not(.dx-focused).dx-#{$widget-name}-group-space, 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..75c6db68554e 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/cell.scss @@ -224,6 +224,12 @@ border-color: $datagrid-row-invalid-border-color; } + // (0,7,1) + .dx-widget:not(.dx-rtl) .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-left, .dx-#{$widget-name}-column-no-border) { + border-left: 1px solid; + border-left-color: $datagrid-border-color; + } + // (0,10,1) .dx-#{$widget-name}-table .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-right-color: $datagrid-hover-bg; @@ -278,6 +284,12 @@ border-left-color: $datagrid-row-selected-border-color; } + // (0,6,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-right, .dx-#{$widget-name}-column-no-border) { + 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 5e7fd72f6ef8..80658baf4d64 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/cell.scss @@ -165,19 +165,9 @@ } // (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 { + .dx-widget:not(.dx-rtl) .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-left, .dx-#{$widget-name}-column-no-border) { 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 @@ -229,6 +219,12 @@ border-left-color: $datagrid-row-selected-border-color; } + // (0,6,1) + .dx-rtl .dx-#{$widget-name}-headers.dx-header-multi-row .dx-row:not(.dx-column-lines) > td:not(.dx-#{$widget-name}-first-header, .dx-#{$widget-name}-sticky-column-border-right, .dx-#{$widget-name}-column-no-border) { + 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..56d824e58e05 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(columnIndex: number, rowIndex = 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..1b4fa5d7f192 --- /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).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(1).getElement()); + const $firstCellOfSecondRow = $(component.getHeaderCell(0, 1).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).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).getElement()); + const $firstCellOfSecondRow = $(component.getHeaderCell(0, 1).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(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 ac89dd174d34..de1a29d77e40 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,35 @@ 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, rowIndex: number) => { + 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 +388,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 +495,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/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..12c8c42c1484 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/types'; 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/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 { From a784fb80130e91ec824704b2c41131bcfbadf5a1 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 17 Mar 2026 19:38:14 +0400 Subject: [PATCH 4/5] Fix lint: use for-of loops in matrix functional test --- .../dataGrid/common/bandColumns/matrix.functional.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts index ce1866a5993a..6d3d358bd45b 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts @@ -28,12 +28,12 @@ async function checkHeaderCellBorders( ): Promise { const headers = dataGrid.getHeaders(); - for (let r = 0; r < expectedRows.length; r += 1) { - const { rowIndex, cells } = expectedRows[r]; + // eslint-disable-next-line no-restricted-syntax + for (const { rowIndex, cells } of expectedRows) { const headerRow = headers.getHeaderRow(rowIndex); - for (let c = 0; c < cells.length; c += 1) { - const { columnIndex, name, expected: [leftWidth, rightWidth] } = cells[c]; + // eslint-disable-next-line no-restricted-syntax + for (const { columnIndex, name, expected: [leftWidth, rightWidth] } of cells) { const { element } = headerRow.getHeaderCell(columnIndex); const borderLeft = await element.getStyleProperty('border-left-width'); From 01fcc19405a70f8db5df2d2e149a058e1cd0d08c Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 18 Mar 2026 03:14:04 +0400 Subject: [PATCH 5/5] Fix e2e tests (part 1) --- .../common/bandColumns/matrix.functional.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts index 6d3d358bd45b..541111cbb938 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/bandColumns/matrix.functional.ts @@ -83,14 +83,14 @@ configs.forEach(( rowIndex: 0, cells: [ { columnIndex: 0, name: 'Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 1] }, - { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, ], }, { rowIndex: 1, cells: [ { columnIndex: 0, name: 'Nested Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 1] }, - { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, ], }, { @@ -101,7 +101,7 @@ configs.forEach(( { columnIndex: 2, name: 'Col3', expected: [1, 1] }, { columnIndex: 3, name: 'Col4', expected: [1, 1] }, { columnIndex: 4, name: 'Col5', expected: [1, 1] }, - { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 0] : [1, 0] }, ], }, ]; @@ -111,26 +111,26 @@ configs.forEach(( { rowIndex: 0, cells: [ - { columnIndex: 0, name: 'Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 0] }, - { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, + { columnIndex: 0, name: 'Band Column 1', expected: rtlEnabled ? [0, 0] : [0, 0] }, + { columnIndex: 3, name: 'Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, ], }, { rowIndex: 1, cells: [ - { columnIndex: 0, name: 'Nested Band Column 1', expected: rtlEnabled ? [1, 0] : [0, 0] }, - { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 0] : [1, 0] }, + { columnIndex: 0, name: 'Nested Band Column 1', expected: rtlEnabled ? [0, 0] : [0, 0] }, + { columnIndex: 3, name: 'Nested Band Column 2', expected: rtlEnabled ? [0, 1] : [1, 0] }, ], }, { rowIndex: 2, cells: [ - { columnIndex: 0, name: 'Col1', expected: rtlEnabled ? [1, 0] : [0, 0] }, - { columnIndex: 1, name: 'Col2', expected: [1, 0] }, - { columnIndex: 2, name: 'Col3', expected: [1, 0] }, - { columnIndex: 3, name: 'Col4', expected: [1, 0] }, - { columnIndex: 4, name: 'Col5', expected: [1, 0] }, - { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 0] : [1, 0] }, + { columnIndex: 0, name: 'Col1', expected: [0, 0] }, + { columnIndex: 1, name: 'Col2', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 2, name: 'Col3', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 3, name: 'Col4', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 4, name: 'Col5', expected: rtlEnabled ? [0, 1] : [1, 0] }, + { columnIndex: 5, name: 'Col6', expected: rtlEnabled ? [0, 1] : [1, 0] }, ], }, ];