From 9771eca5d7e4a032bf71164bd1aab34e4aad5bd9 Mon Sep 17 00:00:00 2001 From: Eugen Zha Date: Wed, 15 Jul 2026 22:18:00 +0300 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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;