diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/columnResizing/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/columnResizing/functional.ts index 4ecc04eea337..068b2faa33d1 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/columnResizing/functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/columnResizing/functional.ts @@ -59,6 +59,68 @@ test('DataGrid – Resize indicator is moved when resizing a grouped column if s }); }); +// T1329677 +test('DataGrid - column width changed via columnOption should be applied immediately, without a repaint (T1329677)', async (t) => { + const dataGrid = new DataGrid('#container'); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.apiColumnOption('Task_Assigned_Employee_ID', 'width', 700); + + const assignedColumnWidth = await dataGrid.getHeaders().getHeaderRow(0) + .getHeaderCell(1).element.clientWidth; + + await t + .expect(assignedColumnWidth) + .within( + 700 - 1, + 700 + 1, + 'columnOption width should be applied immediately, without an explicit repaint', + ); +}).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [{ Task_Subject: 'Test' }], + columnAutoWidth: true, + columns: [ + { dataField: 'Task_Subject' }, + { dataField: 'Task_Assigned_Employee_ID', caption: 'Assigned' }, + ], + }); +}); + +// T1329677 +test('DataGrid - other column width should be updated immediately when another column width is changed via columnOption (T1329677)', async (t) => { + const dataGrid = new DataGrid('#container'); + + await t.expect(dataGrid.isReady()).ok(); + + const firstColumnOldWidth = await dataGrid.getDataCell(0, 0).element.clientWidth; + + await dataGrid.apiColumnOption('Col2', 'width', 200); + + const firstColumnNewWidth = await dataGrid.getDataCell(0, 0).element.clientWidth; + + await t + .expect(firstColumnOldWidth).notEql(firstColumnNewWidth, 'first column width should be changed'); +}).before(async () => { + await createWidget('dxDataGrid', { + dataSource: [{ + Col1: 'Test 1', + Col2: 'Test 2', + Col3: 'Test 3', + Col4: 'Test 4', + }], + width: 400, + columnAutoWidth: true, + columns: [ + { dataField: 'Col1' }, + { dataField: 'Col2' }, + { dataField: 'Col3' }, + { dataField: 'Col4' }, + ], + }); +}); + const tryResizeHeaderInBandArea = ( dataGrid: DataGrid, columnIndex: number, diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts index 31660d62217c..ebe7381db4d2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts @@ -710,6 +710,28 @@ export const fireOptionChanged = function (that: ColumnsController, options) { } }; +const isVisibleWidthChangePendingForColumn = (that: ColumnsController, columnIndex): boolean => { + const columnChanges = that._columnChanges; + + if (!columnChanges?.optionNames?.visibleWidth) { + return false; + } + + return columnChanges.columnIndex === columnIndex + || !!columnChanges.columnIndices?.includes(columnIndex); +}; + +const invalidateStaleVisibleWidths = (that: ColumnsController): void => { + that._columns.forEach((column) => { + const shouldInvalidateVisibleWidth = isDefined(column.visibleWidth) + && !isVisibleWidthChangePendingForColumn(that, column.index); + + if (shouldInvalidateVisibleWidth) { + column.visibleWidth = null; + } + }); +}; + export const columnOptionCore = function (that: ColumnsController, column, optionName, value?, notFireEvent?) { const optionGetter = compileGetter(optionName); const columnIndex = column.index; @@ -735,6 +757,10 @@ export const columnOptionCore = function (that: ColumnsController, column, optio changeType = 'columns'; } + if (optionName === 'width') { + invalidateStaleVisibleWidths(that); + } + const optionSetter = compileSetter(optionName); // @ts-expect-error optionSetter(column, value, { functionsAsIs: true }); 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 0a7de96f0df8..e7c86a684676 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,11 @@ 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); @@ -562,22 +567,25 @@ export default { isDateType, - getSelectionRange(focusedElement) { + getSelectionRange(focusedElement): SelectionRange { try { if (focusedElement) { return { - selectionStart: focusedElement.selectionStart, - selectionEnd: focusedElement.selectionEnd, + selectionStart: isNumeric(focusedElement.selectionStart) ? focusedElement.selectionStart : -1, + selectionEnd: isNumeric(focusedElement.selectionEnd) ? focusedElement.selectionEnd : -1, }; } } catch (e) { /* empty */ } - return {}; + return { + selectionStart: -1, + selectionEnd: -1, + }; }, - setSelectionRange(focusedElement, selectionRange) { + setSelectionRange(focusedElement, selectionRange: SelectionRange): void { try { - if (focusedElement && focusedElement.setSelectionRange) { + if (focusedElement && focusedElement.setSelectionRange && selectionRange.selectionStart >= 0 && selectionRange.selectionEnd >= 0) { focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd); } } 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 4e3ad735a89e..f974234e84ea 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/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); } } }