From f38b2e8d54d56c11e40ae6704d1ac84a329c2888 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 13 Feb 2026 16:32:55 +0400 Subject: [PATCH 1/7] Show column caption in the header cell tooltip instead of a text content --- .../js/__internal/grids/grid_core/views/m_columns_view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 3866c6dcdf8b..30c9133d51e1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -433,7 +433,8 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { const difference = $element[0].scrollWidth - $element[0].clientWidth; if (difference > 0 && !isDefined($element.attr('title'))) { - $element.attr('title', $element.text()); + const hintText = isHeaderRow && column?.caption ? column.caption : $element.text(); + $element.attr('title', hintText); $element.data(CELL_HINT_VISIBLE, true); } } From 7607358860f64e21002390682a1874e276a13de7 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 13 Feb 2026 17:04:51 +0400 Subject: [PATCH 2/7] add tests --- .../__mock__/model/cell/header_cell.ts | 5 ++ .../__tests__/m_column_headers.test.ts | 75 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts index 4b37c22f5f1a..71f75255ce53 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts @@ -4,6 +4,7 @@ const SELECTORS = { headerContent: 'text-content', alignmentRight: 'dx-text-content-alignment-right', alignmentLeft: 'dx-text-content-alignment-left', + sortIndexIcon: 'dx-sort-index-icon', }; export class HeaderCellModel { @@ -39,4 +40,8 @@ export class HeaderCellModel { return 'left'; } } + + public getSortIndexIcon(): HTMLElement | null { + return this.root?.querySelector(`.${SELECTORS.sortIndexIcon}`) ?? null; + } } 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 index 808eb1078b64..f3c16c38e789 100644 --- 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 @@ -36,4 +36,79 @@ describe('Column Headers', () => { expect(component.getHeaderCell(0).getAlignment()).toBe('right'); }); }); + + describe('when cellHintEnabled: true', () => { + const simulateTextOverflow = ( + element: HTMLElement, + scrollWidth: number, + clientWidth: number, + ): void => { + // Mock scrollWidth > clientWidth to simulate text overflow in a narrow column + Object.defineProperty(element, 'scrollWidth', { value: scrollWidth, configurable: true }); + Object.defineProperty(element, 'clientWidth', { value: clientWidth, configurable: true }); + }; + + const simulateHoverEvent = (element: HTMLElement): void => { + // Dispatch mousemove on the element to trigger the tooltip + element.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: 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 headerCell = component.getHeaderCell(0); + const sortIndexIcon = headerCell.getSortIndexIcon() as HTMLElement; + + simulateTextOverflow(sortIndexIcon, 50, 20); + simulateHoverEvent(sortIndexIcon); + + expect($(sortIndexIcon).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); + }); + }); }); From 751ce64b3738f826667d9bd647346e58bc2e2a02 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 13 Feb 2026 19:00:41 +0400 Subject: [PATCH 3/7] code review remarks --- .../__tests__/__mock__/helpers/dom_utils.ts | 19 ++++++++++++ .../__mock__/model/cell/header_cell.ts | 5 ---- .../__tests__/m_column_headers.test.ts | 30 +++++-------------- 3 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/dom_utils.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/dom_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/dom_utils.ts new file mode 100644 index 000000000000..e70dd3138640 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/helpers/dom_utils.ts @@ -0,0 +1,19 @@ +export const simulateTextOverflow = ( + element: HTMLElement, + scrollWidth: number, + clientWidth: number, +): void => { + // Mock scrollWidth > clientWidth to simulate text overflow + Object.defineProperty(element, 'scrollWidth', { value: scrollWidth, configurable: true }); + Object.defineProperty(element, 'clientWidth', { value: clientWidth, configurable: true }); +}; + +export const simulateHoverEvent = ( + element: HTMLElement, + options?: MouseEventInit, +): void => { + element.dispatchEvent(new MouseEvent('mousemove', options ?? { + bubbles: true, + cancelable: true, + })); +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts index 71f75255ce53..4b37c22f5f1a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/header_cell.ts @@ -4,7 +4,6 @@ const SELECTORS = { headerContent: 'text-content', alignmentRight: 'dx-text-content-alignment-right', alignmentLeft: 'dx-text-content-alignment-left', - sortIndexIcon: 'dx-sort-index-icon', }; export class HeaderCellModel { @@ -40,8 +39,4 @@ export class HeaderCellModel { return 'left'; } } - - public getSortIndexIcon(): HTMLElement | null { - return this.root?.querySelector(`.${SELECTORS.sortIndexIcon}`) ?? null; - } } 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 index f3c16c38e789..5efd63150918 100644 --- 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 @@ -3,6 +3,10 @@ import { } from '@jest/globals'; import $ from '@js/core/renderer'; +import { + simulateHoverEvent, + simulateTextOverflow, +} from '../../__tests__/__mock__/helpers/dom_utils'; import { afterTest, beforeTest, @@ -38,24 +42,6 @@ describe('Column Headers', () => { }); describe('when cellHintEnabled: true', () => { - const simulateTextOverflow = ( - element: HTMLElement, - scrollWidth: number, - clientWidth: number, - ): void => { - // Mock scrollWidth > clientWidth to simulate text overflow in a narrow column - Object.defineProperty(element, 'scrollWidth', { value: scrollWidth, configurable: true }); - Object.defineProperty(element, 'clientWidth', { value: clientWidth, configurable: true }); - }; - - const simulateHoverEvent = (element: HTMLElement): void => { - // Dispatch mousemove on the element to trigger the tooltip - element.dispatchEvent(new MouseEvent('mousemove', { - bubbles: true, - cancelable: 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' }], @@ -82,12 +68,12 @@ describe('Column Headers', () => { }); const headerCell = component.getHeaderCell(0); - const sortIndexIcon = headerCell.getSortIndexIcon() as HTMLElement; + const headerCellElement = headerCell.getElement() as HTMLElement; - simulateTextOverflow(sortIndexIcon, 50, 20); - simulateHoverEvent(sortIndexIcon); + simulateTextOverflow(headerCellElement, 50, 20); + simulateHoverEvent(headerCellElement); - expect($(sortIndexIcon).attr('title')).toBe('Position'); + expect($(headerCellElement).attr('title')).toBe('Position'); }); it('should show cell text in the tooltip for non-header rows', async () => { From 4bb9a3b0c97be7f3a1de7b56195822d499f6af87 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 13 Feb 2026 19:20:24 +0400 Subject: [PATCH 4/7] fix incorrect qunit test --- .../tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js index 5a9c64ff2d4b..5f264b0c1cf2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js @@ -324,7 +324,7 @@ QUnit.module('API methods', { firstCellElement.trigger('mousemove'); // assert - assert.strictEqual(firstCellElement.attr('title'), 'Test Test Test Test Test', 'has attribute title in first cell'); + assert.strictEqual(firstCellElement.attr('title'), 'Column 1', 'has attribute title in first cell'); // act const lastCellElement = $table.find('td').last(); From 2792eb2c12c8d7b6aa9f460b46981098ef1ed64f Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Tue, 17 Feb 2026 15:06:50 +0400 Subject: [PATCH 5/7] get tooltip text from header text content instead of data.column.caption --- .../grids/grid_core/views/m_columns_view.ts | 38 +++++++++++++------ .../columnsView.tests.js | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 30c9133d51e1..9b00083f872b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -43,6 +43,7 @@ const SCROLL_CONTAINER_CLASS = 'scroll-container'; const SCROLLABLE_SIMULATED_CLASS = 'scrollable-simulated'; const GROUP_SPACE_CLASS = 'group-space'; const CONTENT_CLASS = 'content'; +const HEADER_TEXT_CONTENT_CLASS = 'text-content'; const TABLE_CLASS = 'table'; const TABLE_FIXED_CLASS = 'table-fixed'; const CONTENT_FIXED_CLASS = 'content-fixed'; @@ -426,17 +427,7 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { && !isGroupCellWithTemplate; if (shouldShowHint) { - if ($element.data(CELL_HINT_VISIBLE)) { - $element.removeAttr('title'); - $element.data(CELL_HINT_VISIBLE, false); - } - - const difference = $element[0].scrollWidth - $element[0].clientWidth; - if (difference > 0 && !isDefined($element.attr('title'))) { - const hintText = isHeaderRow && column?.caption ? column.caption : $element.text(); - $element.attr('title', hintText); - $element.data(CELL_HINT_VISIBLE, true); - } + this._setCellTitleAttribute($cell, isHeaderRow); } })); } @@ -494,6 +485,31 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { return $table; } + private _setCellTitleAttribute($cell: dxElementWrapper, isHeaderRow: boolean): void { + if ($cell.data(CELL_HINT_VISIBLE)) { + $cell.removeAttr('title'); + $cell.data(CELL_HINT_VISIBLE, false); + } + + let $cellContent = $cell; + const headerContentClass = this.addWidgetPrefix(HEADER_TEXT_CONTENT_CLASS); + + if (isHeaderRow && !$cell.hasClass(headerContentClass)) { + $cellContent = $cell.find(`.${headerContentClass}`); + } + + const hasWidthOverflow = $cellContent[0].scrollWidth - $cellContent[0].clientWidth > 0; + + if (!hasWidthOverflow || isDefined($cell.attr('title'))) { + return; + } + + const hintText = $cellContent.text() || $cell.text(); + + $cell.attr('title', hintText); + $cell.data(CELL_HINT_VISIBLE, true); + } + /** * @extended: editing */ diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js index 5f264b0c1cf2..5a9c64ff2d4b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsView.tests.js @@ -324,7 +324,7 @@ QUnit.module('API methods', { firstCellElement.trigger('mousemove'); // assert - assert.strictEqual(firstCellElement.attr('title'), 'Column 1', 'has attribute title in first cell'); + assert.strictEqual(firstCellElement.attr('title'), 'Test Test Test Test Test', 'has attribute title in first cell'); // act const lastCellElement = $table.find('td').last(); From d80efe34382d977bfe51132a156e09a43bd71218 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Tue, 17 Feb 2026 15:34:29 +0400 Subject: [PATCH 6/7] fix copilot review remarks --- .../column_headers/__tests__/m_column_headers.test.ts | 6 +++--- .../js/__internal/grids/grid_core/views/m_columns_view.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) 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 index 5efd63150918..42e87ba55f16 100644 --- 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 @@ -67,10 +67,10 @@ describe('Column Headers', () => { cellHintEnabled: true, }); - const headerCell = component.getHeaderCell(0); - const headerCellElement = headerCell.getElement() as HTMLElement; + const headerCellElement = component.getHeaderCells()[0]; + const headerContentElement = component.getHeaderCell(0).getHeaderContent() as HTMLElement; - simulateTextOverflow(headerCellElement, 50, 20); + simulateTextOverflow(headerContentElement, 50, 20); simulateHoverEvent(headerCellElement); expect($(headerCellElement).attr('title')).toBe('Position'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 9b00083f872b..6db32adb0604 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -495,7 +495,8 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { const headerContentClass = this.addWidgetPrefix(HEADER_TEXT_CONTENT_CLASS); if (isHeaderRow && !$cell.hasClass(headerContentClass)) { - $cellContent = $cell.find(`.${headerContentClass}`); + const $headerContent = $cell.find(`.${headerContentClass}`); + $cellContent = $headerContent.length ? $headerContent : $cell; } const hasWidthOverflow = $cellContent[0].scrollWidth - $cellContent[0].clientWidth > 0; From bbb5f1b1b4f9565795adbf3943def49e49d1de50 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Tue, 17 Feb 2026 20:25:18 +0400 Subject: [PATCH 7/7] return original behavior --- .../js/__internal/grids/grid_core/views/m_columns_view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 6db32adb0604..de48318c3aea 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -427,7 +427,7 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { && !isGroupCellWithTemplate; if (shouldShowHint) { - this._setCellTitleAttribute($cell, isHeaderRow); + this._setCellTitleAttribute($element, isHeaderRow); } })); } @@ -496,7 +496,7 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { if (isHeaderRow && !$cell.hasClass(headerContentClass)) { const $headerContent = $cell.find(`.${headerContentClass}`); - $cellContent = $headerContent.length ? $headerContent : $cell; + $cellContent = $headerContent.length ? $headerContent : $cellContent; } const hasWidthOverflow = $cellContent[0].scrollWidth - $cellContent[0].clientWidth > 0;