From 9771eca5d7e4a032bf71164bd1aab34e4aad5bd9 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 15 Jul 2026 22:18:00 +0300 Subject: [PATCH 01/11] Refactor _synchronizeColumns method --- .../js/__internal/grids/grid_core/m_utils.ts | 20 ++- .../grids/grid_core/views/m_grid_view.ts | 161 +++++++++--------- .../js/__internal/ui/popover/popover.ts | 2 +- 3 files changed, 96 insertions(+), 87 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index f03a205a92c4..af9ca4fd1b3b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -70,6 +70,12 @@ export function isDateType(dataType: string | undefined): boolean { return dataType === 'date' || dataType === 'datetime'; } +export interface SelectionRange { + selectionStart: number; + selectionEnd: number; + selectionDirection?: 'forward' | 'backward' | 'none'; +} + const getIntervalSelector = function () { const data = arguments[1]; const value = this.calculateCellValue(data); @@ -562,23 +568,27 @@ export default { isDateType, - getSelectionRange(focusedElement) { + getSelectionRange(focusedElement): SelectionRange { try { if (focusedElement) { return { selectionStart: focusedElement.selectionStart, selectionEnd: focusedElement.selectionEnd, + selectionDirection: focusedElement.selectionDirection ?? undefined, }; } } catch (e) { /* empty */ } - return {}; + return { + selectionStart: -1, + selectionEnd: -1, + }; }, - setSelectionRange(focusedElement, selectionRange) { + setSelectionRange(focusedElement, selectionRange: SelectionRange): void { try { - if (focusedElement && focusedElement.setSelectionRange) { - focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd); + if (focusedElement && focusedElement.setSelectionRange && selectionRange.selectionStart >= 0 && selectionRange.selectionEnd >= 0) { + focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd, selectionRange.selectionDirection); } } catch (e) { /* empty */ } }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index b121904058a5..09460b162b88 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -23,7 +23,7 @@ import type { ColumnHeadersView } from '../column_headers/m_column_headers'; import type { ColumnsController } from '../columns_controller/m_columns_controller'; import type { DataController } from '../data_controller/m_data_controller'; import modules from '../m_modules'; -import gridCoreUtils from '../m_utils'; +import gridCoreUtils, { type SelectionRange } from '../m_utils'; import type { RowsView } from './m_rows_view'; const BORDERS_CLASS = 'borders'; @@ -78,11 +78,17 @@ const calculateFreeWidthWithCurrentMinWidth = function (that, columnIndex, curre return calculateFreeWidth(that, widths.map((width, index) => (index === columnIndex ? currentMinWidth : width))); }; -const restoreFocus = function (focusedElement, selectionRange) { +const restoreFocus = (focusedElement: Element, selectionRange: SelectionRange): void => { accessibility.hiddenFocus(focusedElement, true); gridCoreUtils.setSelectionRange(focusedElement, selectionRange); }; +interface MaxWidthController { + isModified: boolean; + set: (value: number) => void; + clear: () => void; +} + export class ResizingController extends modules.ViewController { private _refreshSizesHandler: any; @@ -100,8 +106,6 @@ export class ResizingController extends modules.ViewController { private _prevContentMinHeight: any; - private _maxWidth: any; - private _hasWidth: any; private _hasHeight: any; @@ -122,6 +126,26 @@ export class ResizingController extends modules.ViewController { public resizeCompleted!: Callback; + private readonly _maxWidth: MaxWidthController = { + isModified: false, + set: (value): void => { + const $element = this.component.$element(); + + this._maxWidth.isModified = true; + $element.css('maxWidth', value); + }, + clear: (): void => { + const $element = this.component.$element(); + + if (!this._maxWidth.isModified || !$element || !$element.get(0)) { + return; + } + + this._maxWidth.isModified = false; + $element[0].style.maxWidth = ''; + }, + }; + protected callbackNames() { return ['resizeCompleted']; } @@ -338,85 +362,55 @@ export class ResizingController extends modules.ViewController { } } - private _synchronizeColumns() { - const columnsController = this._columnsController; - const visibleColumns = columnsController.getVisibleColumns(); - const columnAutoWidth = this.option('columnAutoWidth'); - const hasUndefinedColumnWidth = visibleColumns.some((column) => !isDefined(column.width)); - let needBestFit = this._needBestFit(); - let hasMinWidth = false; - let resetBestFitMode; - let isColumnWidthsCorrected = false; - let resultWidths: any[] = []; - let focusedElement; - let selectionRange; + private _enableTemporaryBestFitMode(): () => void { + const $element = this.component.$element(); + const focusedElement = domAdapter.getActiveElement($element.get(0) as HTMLElement | null); + const selectionRange = gridCoreUtils.getSelectionRange(focusedElement); - const normalizeWidthsByExpandColumns = function () { - let expandColumnWidth; + this._toggleBestFitMode(true); - each(visibleColumns, (index, column) => { - if (column.type === 'groupExpand') { - expandColumnWidth = resultWidths[index]; - } - }); + return (): void => { + this._toggleBestFitMode(false); - each(visibleColumns, (index, column) => { - if (column.type === 'groupExpand' && expandColumnWidth) { - resultWidths[index] = expandColumnWidth; - } - }); - }; + if (focusedElement && focusedElement !== domAdapter.getActiveElement()) { + const isFocusOutsideWindow = getBoundingRect(focusedElement).bottom < 0; - !needBestFit && each(visibleColumns, (index, column) => { - if (column.width === 'auto') { - needBestFit = true; - return false; + if (!isFocusOutsideWindow) { + restoreFocus(focusedElement, selectionRange); + } } - return undefined; - }); + }; + } - each(visibleColumns, (index, column) => { - if (column.minWidth) { - hasMinWidth = true; - return false; - } - return undefined; - }); + private _synchronizeColumns():void { + const columnsController = this._columnsController; + const visibleColumns = columnsController.getVisibleColumns(); + const columnAutoWidth = this.option('columnAutoWidth') as boolean; + const hasUndefinedColumnWidth = visibleColumns.some((column) => !isDefined(column.width)); + const needBestFit = this._needBestFit() || visibleColumns.some((column) => column.width === 'auto'); + const hasMinWidth = visibleColumns.some((column) => !!column.minWidth); + // Prepare for measurement this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354 - this._setVisibleWidths(visibleColumns, []); - - const $element = this.component.$element(); - - if (needBestFit) { - // @ts-expect-error - focusedElement = domAdapter.getActiveElement($element.get(0)); - selectionRange = gridCoreUtils.getSelectionRange(focusedElement); - this._toggleBestFitMode(true); - resetBestFitMode = true; - } - - if ($element && $element.get(0) && this._maxWidth) { - delete this._maxWidth; - $element[0].style.maxWidth = ''; - } + const restoreAfterBestFitMode = needBestFit && this._enableTemporaryBestFitMode(); + this._maxWidth.clear(); // eslint-disable-next-line @typescript-eslint/no-floating-promises deferUpdate(() => { - if (needBestFit) { + let resultWidths: (number | string | undefined)[] = []; + + if (needBestFit || hasMinWidth) { resultWidths = this._getBestFitWidths(); + } - each(visibleColumns, (index, column) => { + each(visibleColumns, (index, column) => { + if (needBestFit) { const columnId = columnsController.getColumnId(column); columnsController.columnOption(columnId, 'bestFitWidth', resultWidths[index], true); - }); - } else if (hasMinWidth) { - resultWidths = this._getBestFitWidths(); - } + } - each(visibleColumns, function (index) { - const { width } = this; + const { width } = column; if (width !== 'auto') { if (isDefined(width)) { resultWidths[index] = isNumeric(width) || isPixelWidth(width) ? parseFloat(width) : width; @@ -426,21 +420,14 @@ export class ResizingController extends modules.ViewController { } }); - if (resetBestFitMode) { - this._toggleBestFitMode(false); - resetBestFitMode = false; - if (focusedElement && focusedElement !== domAdapter.getActiveElement()) { - const isFocusOutsideWindow = getBoundingRect(focusedElement).bottom < 0; - if (!isFocusOutsideWindow) { - restoreFocus(focusedElement, selectionRange); - } - } + if (restoreAfterBestFitMode) { + restoreAfterBestFitMode(); } - isColumnWidthsCorrected = this._correctColumnWidths(resultWidths, visibleColumns); + const isColumnWidthsCorrected = this._correctColumnWidths(resultWidths, visibleColumns); if (columnAutoWidth) { - normalizeWidthsByExpandColumns(); + this._normalizeWidthsByExpandColumns(resultWidths, visibleColumns); if (this._needStretch()) { this._processStretch(resultWidths, visibleColumns); } @@ -478,6 +465,21 @@ export class ResizingController extends modules.ViewController { return freeWidth / columnCountWithoutWidth; } + private readonly _normalizeWidthsByExpandColumns = (resultWidths, visibleColumns): void => { + const expandColumnIndex = visibleColumns.findIndex((column) => column.type === 'groupExpand'); + const expandColumnWidth = resultWidths[expandColumnIndex]; + + if (!isDefined(expandColumnWidth)) { + return; + } + + each(visibleColumns, (index, column) => { + if (column.type === 'groupExpand') { + resultWidths[index] = expandColumnWidth; + } + }); + }; + /** * @extended: adaptivity */ @@ -487,7 +489,6 @@ export class ResizingController extends modules.ViewController { let hasPercentWidth = false; let hasAutoWidth = false; let isColumnWidthsCorrected = false; - const $element = that.component.$element(); const hasWidth = that._hasWidth; for (i = 0; i < visibleColumns.length; i++) { @@ -540,9 +541,7 @@ export class ResizingController extends modules.ViewController { if (hasWidth === false && !hasPercentWidth) { const borderWidth = gridCoreUtils.getComponentBorderWidth(this, $rowsViewElement); - that._maxWidth = totalWidth + scrollbarWidth + borderWidth; - - $element.css('maxWidth', that._maxWidth); + that._maxWidth.set(totalWidth + scrollbarWidth + borderWidth); } } } diff --git a/packages/devextreme/js/__internal/ui/popover/popover.ts b/packages/devextreme/js/__internal/ui/popover/popover.ts index 6e5676cf2189..bf60b74ff3ab 100644 --- a/packages/devextreme/js/__internal/ui/popover/popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover.ts @@ -200,7 +200,7 @@ class Popover< const { visible } = this.option(); const overlayStack = this._overlayStack(); - const isTopOverlay = overlayStack[overlayStack.length - 1] === this; + const isTopOverlay = overlayStack[overlayStack.length - 1] === this as unknown; if (normalizeKeyName(e) === ESC_KEY_NAME && visible && isTopOverlay) { // eslint-disable-next-line @typescript-eslint/no-floating-promises From 3d602d34cd330568736218100c8f7809f64f67b2 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 15 Jul 2026 22:46:50 +0300 Subject: [PATCH 02/11] fix(popover): remove unnecessary type assertion for overlay stack check --- packages/devextreme/js/__internal/ui/popover/popover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/popover/popover.ts b/packages/devextreme/js/__internal/ui/popover/popover.ts index bf60b74ff3ab..6e5676cf2189 100644 --- a/packages/devextreme/js/__internal/ui/popover/popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover.ts @@ -200,7 +200,7 @@ class Popover< const { visible } = this.option(); const overlayStack = this._overlayStack(); - const isTopOverlay = overlayStack[overlayStack.length - 1] === this as unknown; + const isTopOverlay = overlayStack[overlayStack.length - 1] === this; if (normalizeKeyName(e) === ESC_KEY_NAME && visible && isTopOverlay) { // eslint-disable-next-line @typescript-eslint/no-floating-promises From 9957424bc2281f4889aacb059871ebe9ec82c7c6 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 15 Jul 2026 22:56:12 +0300 Subject: [PATCH 03/11] fix(m_utils): handle non-numeric selectionStart and selectionEnd values --- packages/devextreme/js/__internal/grids/grid_core/m_utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index af9ca4fd1b3b..be482ce2fb82 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -572,8 +572,8 @@ export default { try { if (focusedElement) { return { - selectionStart: focusedElement.selectionStart, - selectionEnd: focusedElement.selectionEnd, + selectionStart: isNumeric(focusedElement.selectionStart) ? focusedElement.selectionStart : -1, + selectionEnd: isNumeric(focusedElement.selectionEnd) ? focusedElement.selectionEnd : -1, selectionDirection: focusedElement.selectionDirection ?? undefined, }; } From 32ad957e421a0b4a220a67ea863777e8349b93eb Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 15 Jul 2026 22:58:59 +0300 Subject: [PATCH 04/11] fix(grid_view): correct spacing in _synchronizeColumns method --- .../js/__internal/grids/grid_core/views/m_grid_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 09460b162b88..88b9da1c35ed 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -382,7 +382,7 @@ export class ResizingController extends modules.ViewController { }; } - private _synchronizeColumns():void { + private _synchronizeColumns(): void { const columnsController = this._columnsController; const visibleColumns = columnsController.getVisibleColumns(); const columnAutoWidth = this.option('columnAutoWidth') as boolean; From dfde56ef47d38229c6256679306977e1dd56698c Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Tue, 21 Jul 2026 18:08:05 +0300 Subject: [PATCH 05/11] fix(m_utils): remove selectionDirection from SelectionRange interface and related logic --- packages/devextreme/js/__internal/grids/grid_core/m_utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index be482ce2fb82..8dec78937234 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -73,7 +73,6 @@ export function isDateType(dataType: string | undefined): boolean { export interface SelectionRange { selectionStart: number; selectionEnd: number; - selectionDirection?: 'forward' | 'backward' | 'none'; } const getIntervalSelector = function () { @@ -574,7 +573,6 @@ export default { return { selectionStart: isNumeric(focusedElement.selectionStart) ? focusedElement.selectionStart : -1, selectionEnd: isNumeric(focusedElement.selectionEnd) ? focusedElement.selectionEnd : -1, - selectionDirection: focusedElement.selectionDirection ?? undefined, }; } } catch (e) { /* empty */ } @@ -588,7 +586,7 @@ export default { setSelectionRange(focusedElement, selectionRange: SelectionRange): void { try { if (focusedElement && focusedElement.setSelectionRange && selectionRange.selectionStart >= 0 && selectionRange.selectionEnd >= 0) { - focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd, selectionRange.selectionDirection); + focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd); } } catch (e) { /* empty */ } }, From 781388e0462d5be89d9a9e08dcfcd429bb6f817b Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Tue, 22 Sep 2026 14:30:13 +0300 Subject: [PATCH 06/11] Update after review --- ...normalize_widths_by_expand_columns.test.ts | 81 ++++++++++++++++++ .../grids/grid_core/views/m_grid_view.ts | 84 +++++++++++-------- 2 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts new file mode 100644 index 000000000000..ce9e39942abc --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from '@jest/globals'; + +import type { Column } from '../../columns_controller/types'; +import { ResizingController } from '../m_grid_view'; + +type ColumnWidth = number | string | undefined; + +// NOTE: the method is private, so it is picked from the prototype to be tested in isolation. +const resizingControllerPrototype = ResizingController.prototype as unknown as { + _normalizeWidthsByExpandColumns: ( + resultWidths: ColumnWidth[], + visibleColumns: Column[], + ) => void; +}; + +const normalizeWidthsByExpandColumns = ( + resultWidths: ColumnWidth[], + visibleColumns: Column[], +): ColumnWidth[] => { + resizingControllerPrototype._normalizeWidthsByExpandColumns(resultWidths, visibleColumns); + + return resultWidths; +}; + +const expandColumn = (): Column => ({ type: 'groupExpand', command: 'expand' } as Column); +const dataColumn = (dataField: string): Column => ({ dataField } as Column); + +describe('ResizingController._normalizeWidthsByExpandColumns', () => { + it('leaves the widths as is when there are no expand columns', () => { + const columns = [dataColumn('a'), dataColumn('b')]; + + expect(normalizeWidthsByExpandColumns([100, 200], columns)).toEqual([100, 200]); + }); + + it('leaves the widths as is when there is a single expand column', () => { + const columns = [expandColumn(), dataColumn('a')]; + + expect(normalizeWidthsByExpandColumns([30, 200], columns)).toEqual([30, 200]); + }); + + // NOTE: all groupExpand columns share a single column id (command:expand), so the width + // of the last one is the value that _setVisibleWidths actually applies to all of them. + it('applies the width of the LAST expand column to every expand column', () => { + const columns = [expandColumn(), expandColumn(), dataColumn('a')]; + + expect(normalizeWidthsByExpandColumns([21, 30, 200], columns)).toEqual([30, 30, 200]); + }); + + it('normalizes expand columns that are not adjacent to each other', () => { + const columns = [ + dataColumn('a'), + expandColumn(), + dataColumn('b'), + expandColumn(), + dataColumn('c'), + ]; + + expect(normalizeWidthsByExpandColumns([100, 21, 200, 30, 300], columns)) + .toEqual([100, 30, 200, 30, 300]); + }); + + it('ignores the detailExpand column', () => { + const columns = [ + { type: 'detailExpand', command: 'expand' } as Column, + expandColumn(), + expandColumn(), + ]; + + expect(normalizeWidthsByExpandColumns([15, 21, 30], columns)).toEqual([15, 30, 30]); + }); + + // NOTE: a falsy width means the column could not be measured (e.g. the grid is hidden). + it.each([ + ['zero', 0], + ['undefined', undefined], + ])('keeps the measured widths when the last expand column width is %s', (_, width) => { + const columns = [expandColumn(), expandColumn(), dataColumn('a')]; + + expect(normalizeWidthsByExpandColumns([21, width, 200], columns)).toEqual([21, width, 200]); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 69e810804bed..465687847a71 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -21,7 +21,9 @@ import { A11yStatusContainerComponent } from '@ts/grids/grid_core/views/a11y_sta import type { FooterView } from '../../data_grid/summary/m_summary'; import type { AdaptiveColumnsController } from '../adaptivity/m_adaptivity'; import type { ColumnHeadersView } from '../column_headers/m_column_headers'; +import { GROUP_COMMAND_COLUMN_NAME } from '../columns_controller/const'; import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import type { DataController } from '../data_controller/data_controller'; import type { DataChange } from '../data_controller/types'; import type { DataSourceController } from '../data_source/data_source_controller'; @@ -37,6 +39,8 @@ const GROUP_ROW_SELECTOR = 'tr.dx-group-row'; const HIDDEN_COLUMNS_WIDTH = 'adaptiveHidden'; +type ColumnWidth = number | string | undefined; + const VIEW_NAMES = [ 'columnsSeparatorView', 'blockSeparatorView', @@ -86,12 +90,6 @@ const restoreFocus = (focusedElement: Element, selectionRange: SelectionRange): gridCoreUtils.setSelectionRange(focusedElement, selectionRange); }; -interface MaxWidthController { - isModified: boolean; - set: (value: number) => void; - clear: () => void; -} - export class ResizingController extends modules.ViewController { private _refreshSizesHandler: any; @@ -131,25 +129,7 @@ export class ResizingController extends modules.ViewController { public resizeCompleted!: Callback; - private readonly _maxWidth: MaxWidthController = { - isModified: false, - set: (value): void => { - const $element = this.component.$element(); - - this._maxWidth.isModified = true; - $element.css('maxWidth', value); - }, - clear: (): void => { - const $element = this.component.$element(); - - if (!this._maxWidth.isModified || !$element || !$element.get(0)) { - return; - } - - this._maxWidth.isModified = false; - $element[0].style.maxWidth = ''; - }, - }; + private _isMaxWidthSet = false; protected callbackNames() { return ['resizeCompleted']; @@ -369,6 +349,25 @@ export class ResizingController extends modules.ViewController { } } + private _setMaxWidth(value: number): void { + this._isMaxWidthSet = true; + this.component.$element().css('maxWidth', value); + } + + private _clearMaxWidth(): void { + if (!this._isMaxWidthSet) { + return; + } + + this._isMaxWidthSet = false; + + const element = this.component.$element().get(0) as HTMLElement | undefined; + + if (element) { + element.style.maxWidth = ''; + } + } + private _enableTemporaryBestFitMode(): () => void { const $element = this.component.$element(); const focusedElement = domAdapter.getActiveElement($element.get(0) as HTMLElement | null); @@ -401,11 +400,11 @@ export class ResizingController extends modules.ViewController { this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354 this._setVisibleWidths(visibleColumns, []); const restoreAfterBestFitMode = needBestFit && this._enableTemporaryBestFitMode(); - this._maxWidth.clear(); + this._clearMaxWidth(); // eslint-disable-next-line @typescript-eslint/no-floating-promises deferUpdate(() => { - let resultWidths: (number | string | undefined)[] = []; + let resultWidths: ColumnWidth[] = []; if (needBestFit || hasMinWidth) { resultWidths = this._getBestFitWidths(); @@ -472,20 +471,31 @@ export class ResizingController extends modules.ViewController { return freeWidth / columnCountWithoutWidth; } - private readonly _normalizeWidthsByExpandColumns = (resultWidths, visibleColumns): void => { - const expandColumnIndex = visibleColumns.findIndex((column) => column.type === 'groupExpand'); - const expandColumnWidth = resultWidths[expandColumnIndex]; + private _normalizeWidthsByExpandColumns( + resultWidths: ColumnWidth[], + visibleColumns: Column[], + ): void { + const expandColumnIndexes = visibleColumns.reduce( + (indexes, column, index) => ( + column.type === GROUP_COMMAND_COLUMN_NAME ? [...indexes, index] : indexes + ), + [], + ); + + // NOTE: all groupExpand columns share a single column id (command:expand), + // so the width of the last one is what actually gets applied to all of them. + const expandColumnWidth = resultWidths[expandColumnIndexes.at(-1) ?? -1]; - if (!isDefined(expandColumnWidth)) { + // NOTE: a falsy width means the column could not be measured (e.g. the grid is hidden), + // in that case the measured widths are kept as is. + if (!expandColumnWidth) { return; } - each(visibleColumns, (index, column) => { - if (column.type === 'groupExpand') { - resultWidths[index] = expandColumnWidth; - } + expandColumnIndexes.forEach((index) => { + resultWidths[index] = expandColumnWidth; }); - }; + } /** * @extended: adaptivity @@ -548,7 +558,7 @@ export class ResizingController extends modules.ViewController { if (hasWidth === false && !hasPercentWidth) { const borderWidth = gridCoreUtils.getComponentBorderWidth(this, $rowsViewElement); - that._maxWidth.set(totalWidth + scrollbarWidth + borderWidth); + that._setMaxWidth(totalWidth + scrollbarWidth + borderWidth); } } } From 1312c25ea13ff8a5e4cccbc19d4b27ab86d8b0f6 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Tue, 22 Sep 2026 14:56:00 +0300 Subject: [PATCH 07/11] refactor(ResizingController): simplify expand column width calculation --- .../grids/grid_core/views/m_grid_view.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 465687847a71..6e5fafd88030 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -475,25 +475,27 @@ export class ResizingController extends modules.ViewController { resultWidths: ColumnWidth[], visibleColumns: Column[], ): void { - const expandColumnIndexes = visibleColumns.reduce( - (indexes, column, index) => ( - column.type === GROUP_COMMAND_COLUMN_NAME ? [...indexes, index] : indexes - ), - [], + const isExpandColumn = (column: Column): boolean => column.type === GROUP_COMMAND_COLUMN_NAME; + + const lastExpandColumnIndex = visibleColumns.reduce( + (result, column, index) => (isExpandColumn(column) ? index : result), + -1, ); - // NOTE: all groupExpand columns share a single column id (command:expand), - // so the width of the last one is what actually gets applied to all of them. - const expandColumnWidth = resultWidths[expandColumnIndexes.at(-1) ?? -1]; + if (lastExpandColumnIndex < 0) { + return; + } + + const expandColumnWidth = resultWidths[lastExpandColumnIndex]; - // NOTE: a falsy width means the column could not be measured (e.g. the grid is hidden), - // in that case the measured widths are kept as is. if (!expandColumnWidth) { return; } - expandColumnIndexes.forEach((index) => { - resultWidths[index] = expandColumnWidth; + visibleColumns.forEach((column, index) => { + if (isExpandColumn(column)) { + resultWidths[index] = expandColumnWidth; + } }); } From e86dc6d31ae6d53fc357cefbd1ed9aa2d6771b5f Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Tue, 22 Sep 2026 15:25:32 +0300 Subject: [PATCH 08/11] refactor(ResizingController): remove redundant comment before measurement preparation --- .../js/__internal/grids/grid_core/views/m_grid_view.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 6e5fafd88030..ddf0012c961b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -396,7 +396,6 @@ export class ResizingController extends modules.ViewController { const needBestFit = this._needBestFit() || visibleColumns.some((column) => column.width === 'auto'); const hasMinWidth = visibleColumns.some((column) => !!column.minWidth); - // Prepare for measurement this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354 this._setVisibleWidths(visibleColumns, []); const restoreAfterBestFitMode = needBestFit && this._enableTemporaryBestFitMode(); From 57b55a3888f7a43d9c2ff9af00e3655814a350e4 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 23 Sep 2026 19:43:09 +0300 Subject: [PATCH 09/11] feat(SelectionRange): add SelectionRange interface and update imports --- .../js/__internal/grids/grid_core/m_types.ts | 5 +++++ .../js/__internal/grids/grid_core/m_utils.ts | 7 +------ .../grids/grid_core/views/m_grid_view.ts | 14 +++++++------- .../js/__internal/grids/grid_core/views/types.ts | 2 ++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 529353610025..242bdcc824c6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -34,6 +34,11 @@ export interface Coordinates { y: number; } +export interface SelectionRange { + selectionStart: number; + selectionEnd: number; +} + // todo: move to upper .d.ts type OptionsMethod = (() => TOptions) & ((options: TOptions) => void) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index fb8fbb4840a9..ca64420e7f46 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -23,7 +23,7 @@ import sharedFiltering from '@js/ui/shared/filtering'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; -import type { ColumnPoint } from '@ts/grids/grid_core/m_types'; +import type { ColumnPoint, SelectionRange } from '@ts/grids/grid_core/m_types'; import { AI_COLUMN_NAME } from './ai_column/const'; import type DataSourceAdapter from './data_source_adapter/m_data_source_adapter'; @@ -72,11 +72,6 @@ export function isDateType(dataType: string | undefined): boolean { return dataType === 'date' || dataType === 'datetime'; } -export interface SelectionRange { - selectionStart: number; - selectionEnd: number; -} - const getIntervalSelector = function () { const data = arguments[1]; const value = this.calculateCellValue(data); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index ddf0012c961b..b8e9d434813c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -28,8 +28,10 @@ import type { DataController } from '../data_controller/data_controller'; import type { DataChange } from '../data_controller/types'; import type { DataSourceController } from '../data_source/data_source_controller'; import modules from '../m_modules'; -import gridCoreUtils, { type SelectionRange } from '../m_utils'; +import type { SelectionRange } from '../m_types'; +import gridCoreUtils from '../m_utils'; import type { RowsView } from './m_rows_view'; +import type { ColumnWidth } from './types'; const BORDERS_CLASS = 'borders'; const TABLE_FIXED_CLASS = 'table-fixed'; @@ -39,8 +41,6 @@ const GROUP_ROW_SELECTOR = 'tr.dx-group-row'; const HIDDEN_COLUMNS_WIDTH = 'adaptiveHidden'; -type ColumnWidth = number | string | undefined; - const VIEW_NAMES = [ 'columnsSeparatorView', 'blockSeparatorView', @@ -129,7 +129,7 @@ export class ResizingController extends modules.ViewController { public resizeCompleted!: Callback; - private _isMaxWidthSet = false; + private isMaxWidthSet = false; protected callbackNames() { return ['resizeCompleted']; @@ -350,16 +350,16 @@ export class ResizingController extends modules.ViewController { } private _setMaxWidth(value: number): void { - this._isMaxWidthSet = true; + this.isMaxWidthSet = true; this.component.$element().css('maxWidth', value); } private _clearMaxWidth(): void { - if (!this._isMaxWidthSet) { + if (!this.isMaxWidthSet) { return; } - this._isMaxWidthSet = false; + this.isMaxWidthSet = false; const element = this.component.$element().get(0) as HTMLElement | undefined; diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/types.ts b/packages/devextreme/js/__internal/grids/grid_core/views/types.ts index a2f0306d5705..485c13a4f5ed 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/types.ts @@ -6,3 +6,5 @@ export type RowsViewScrollEvent = Partial> & { scrollOffset: { top: number; left: number }; forceUpdateScrollPosition?: boolean; }; + +export type ColumnWidth = number | string | undefined; From 9f8361666d0203f56bc1a8cf2a6eb264439c44f7 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 23 Sep 2026 20:09:51 +0300 Subject: [PATCH 10/11] refactor(ResizingController): rename private methods for consistency --- ...d_view.normalize_widths_by_expand_columns.test.ts | 6 +++--- .../__internal/grids/grid_core/views/m_grid_view.ts | 12 ++++++------ .../dataGrid.resizing.tests.js | 10 +++++----- .../filtering.integration.tests.js | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts index ce9e39942abc..16b8b9c247de 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.normalize_widths_by_expand_columns.test.ts @@ -7,7 +7,7 @@ type ColumnWidth = number | string | undefined; // NOTE: the method is private, so it is picked from the prototype to be tested in isolation. const resizingControllerPrototype = ResizingController.prototype as unknown as { - _normalizeWidthsByExpandColumns: ( + normalizeWidthsByExpandColumns: ( resultWidths: ColumnWidth[], visibleColumns: Column[], ) => void; @@ -17,7 +17,7 @@ const normalizeWidthsByExpandColumns = ( resultWidths: ColumnWidth[], visibleColumns: Column[], ): ColumnWidth[] => { - resizingControllerPrototype._normalizeWidthsByExpandColumns(resultWidths, visibleColumns); + resizingControllerPrototype.normalizeWidthsByExpandColumns(resultWidths, visibleColumns); return resultWidths; }; @@ -25,7 +25,7 @@ const normalizeWidthsByExpandColumns = ( const expandColumn = (): Column => ({ type: 'groupExpand', command: 'expand' } as Column); const dataColumn = (dataField: string): Column => ({ dataField } as Column); -describe('ResizingController._normalizeWidthsByExpandColumns', () => { +describe('ResizingController.normalizeWidthsByExpandColumns', () => { it('leaves the widths as is when there are no expand columns', () => { const columns = [dataColumn('a'), dataColumn('b')]; diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index b8e9d434813c..de412b8b9c3a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -368,7 +368,7 @@ export class ResizingController extends modules.ViewController { } } - private _enableTemporaryBestFitMode(): () => void { + private enableTemporaryBestFitMode(): () => void { const $element = this.component.$element(); const focusedElement = domAdapter.getActiveElement($element.get(0) as HTMLElement | null); const selectionRange = gridCoreUtils.getSelectionRange(focusedElement); @@ -388,7 +388,7 @@ export class ResizingController extends modules.ViewController { }; } - private _synchronizeColumns(): void { + private synchronizeColumns(): void { const columnsController = this._columnsController; const visibleColumns = columnsController.getVisibleColumns(); const columnAutoWidth = this.option('columnAutoWidth') as boolean; @@ -398,7 +398,7 @@ export class ResizingController extends modules.ViewController { this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354 this._setVisibleWidths(visibleColumns, []); - const restoreAfterBestFitMode = needBestFit && this._enableTemporaryBestFitMode(); + const restoreAfterBestFitMode = needBestFit && this.enableTemporaryBestFitMode(); this._clearMaxWidth(); // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -432,7 +432,7 @@ export class ResizingController extends modules.ViewController { const isColumnWidthsCorrected = this._correctColumnWidths(resultWidths, visibleColumns); if (columnAutoWidth) { - this._normalizeWidthsByExpandColumns(resultWidths, visibleColumns); + this.normalizeWidthsByExpandColumns(resultWidths, visibleColumns); if (this._needStretch()) { this._processStretch(resultWidths, visibleColumns); } @@ -470,7 +470,7 @@ export class ResizingController extends modules.ViewController { return freeWidth / columnCountWithoutWidth; } - private _normalizeWidthsByExpandColumns( + private normalizeWidthsByExpandColumns( resultWidths: ColumnWidth[], visibleColumns: Column[], ): void { @@ -733,7 +733,7 @@ export class ResizingController extends modules.ViewController { when(prevResult).always(() => { deferRender(() => { if (that._dataController.isLoaded()) { - that._synchronizeColumns(); + that.synchronizeColumns(); } // IE11 that._resetGroupElementHeight(); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.resizing.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.resizing.tests.js index 1aac2811455c..5ac896cb34b5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.resizing.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.resizing.tests.js @@ -1230,13 +1230,13 @@ QUnit.module('API Methods', baseModuleConfig, () => { dataSource: [{ field1: 1, field2: 2, field3: 3 }] }); - sinon.spy(dataGrid.getController('resizing'), '_synchronizeColumns'); + sinon.spy(dataGrid.getController('resizing'), 'synchronizeColumns'); // act resizeCallbacks.fire(); // assert - assert.equal(dataGrid.getController('resizing')._synchronizeColumns.callCount, 0, 'synchronizeColumns is not called'); + assert.equal(dataGrid.getController('resizing').synchronizeColumns.callCount, 0, 'synchronizeColumns is not called'); // act @@ -1244,7 +1244,7 @@ QUnit.module('API Methods', baseModuleConfig, () => { resizeCallbacks.fire(); // assert - assert.equal(dataGrid.getController('resizing')._synchronizeColumns.callCount, 1, 'synchronizeColumns is called'); + assert.equal(dataGrid.getController('resizing').synchronizeColumns.callCount, 1, 'synchronizeColumns is called'); }); // T372519 @@ -1264,7 +1264,7 @@ QUnit.module('API Methods', baseModuleConfig, () => { const rowsViewHeight = $('#dataGrid .dx-datagrid-rowsview').height(); - sinon.spy(dataGrid.getController('resizing'), '_synchronizeColumns'); + sinon.spy(dataGrid.getController('resizing'), 'synchronizeColumns'); // act $('#qunit-fixture').hide(); @@ -1272,7 +1272,7 @@ QUnit.module('API Methods', baseModuleConfig, () => { $('#qunit-fixture').show(); // assert - assert.equal(dataGrid.getController('resizing')._synchronizeColumns.callCount, 0, 'synchronizeColumns is not called'); + assert.equal(dataGrid.getController('resizing').synchronizeColumns.callCount, 0, 'synchronizeColumns is not called'); assert.equal($('#dataGrid .dx-datagrid-rowsview').height(), rowsViewHeight, 'rowsView height is not changed'); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filtering.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filtering.integration.tests.js index 76d6fe2ed6e4..181377bcf6f9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filtering.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/filtering.integration.tests.js @@ -194,7 +194,7 @@ QUnit.module('Initialization', baseModuleConfig, () => { assert.equal(dataGrid.getVisibleRows().length, 1, 'rows are filtered'); }); - QUnit.testInActiveWindow('Filter row editor should have focus after _synchronizeColumns (T638737)', function(assert) { + QUnit.testInActiveWindow('Filter row editor should have focus after synchronizeColumns (T638737)', function(assert) { $('#qunit-fixture').css('position', 'static'); // arrange, act const dataGrid = createDataGrid({ From cdce32f8ff150d43f2f8e280aa445ba438c14005 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 23 Sep 2026 20:27:39 +0300 Subject: [PATCH 11/11] refactor(ResizingController): rename private method _setMaxWidth to setMaxWidth for consistency --- .../js/__internal/grids/grid_core/views/m_grid_view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index de412b8b9c3a..c30560361fd2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -349,7 +349,7 @@ export class ResizingController extends modules.ViewController { } } - private _setMaxWidth(value: number): void { + private setMaxWidth(value: number): void { this.isMaxWidthSet = true; this.component.$element().css('maxWidth', value); } @@ -559,7 +559,7 @@ export class ResizingController extends modules.ViewController { if (hasWidth === false && !hasPercentWidth) { const borderWidth = gridCoreUtils.getComponentBorderWidth(this, $rowsViewElement); - that._setMaxWidth(totalWidth + scrollbarWidth + borderWidth); + that.setMaxWidth(totalWidth + scrollbarWidth + borderWidth); } } }