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/__tests__/columns_controller.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.integration.test.ts index 37748edb8578..8ce3c92fdf98 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/__tests__/columns_controller.integration.test.ts @@ -8,6 +8,7 @@ import { beforeTest, createDataGrid, } from '../../__tests__/__mock__/helpers/utils'; +import { HIDDEN_COLUMNS_WIDTH } from '../../adaptivity/const'; describe('getFilteringColumns', () => { beforeEach(beforeTest); @@ -138,4 +139,153 @@ describe('Bugs', () => { expect(dataCellsArray.length).toBe(1); }); }); + + describe('T1329677 - DataGrid - Column width changes are not applied immediately', () => { + it('should invalidate calculated widths when a column width changes through columnOption', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1', field2: 'value 2', field3: 'value 3' }], + columns: ['field1', 'field2', 'field3'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 100); + columnsController.columnOption(1, 'visibleWidth', 110); + columnsController.columnOption(2, 'visibleWidth', 120); + + instance.columnOption(1, 'width', 150); + + expect(columnsController.getColumns().map((column) => column.visibleWidth)).toEqual([ + null, null, null, + ]); + }); + + it('should preserve an adaptive-hidden marker when a column width changes through columnOption', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1' }], + columns: ['field1'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', HIDDEN_COLUMNS_WIDTH); + + instance.columnOption(0, 'width', 150); + + expect(columnsController.columnOption(0, 'visibleWidth')).toBe(HIDDEN_COLUMNS_WIDTH); + }); + + it('should invalidate an auto visible width when a column width changes through columnOption', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1' }], + columns: ['field1'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 'auto'); + + instance.columnOption(0, 'width', 150); + + expect(columnsController.columnOption(0, 'visibleWidth')).toBeNull(); + }); + + it('should invalidate calculated widths of command columns when another column width changes through columnOption', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1' }], + columns: ['field1'], + }); + const columnsController = instance.getController('columns'); + + columnsController.addCommandColumn({ command: 'test', width: 'auto' }); + columnsController.columnOption('command:test', 'visibleWidth', 100); + + instance.columnOption('field1', 'width', 150); + + expect(columnsController.columnOption('command:test', 'visibleWidth')).toBeNull(); + }); + + it('should preserve calculated widths of unrelated columns when applying resolved dimensions', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1', field2: 'value 2', field3: 'value 3' }], + columns: ['field1', 'field2', 'field3'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 100); + columnsController.columnOption(1, 'visibleWidth', 110); + columnsController.columnOption(2, 'visibleWidth', 120); + + columnsController.updateColumnDimensions([{ + columnIndex: 1, + visibleWidth: null, + width: 150, + }]); + + expect(columnsController.columnOption(1, 'width')).toBe(150); + expect(columnsController.getColumns().map((column) => column.visibleWidth)).toEqual([ + 100, null, 120, + ]); + }); + + it('should invalidate a stale visible width when another option changed the same column in the batch', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1', field2: 'value 2', field3: 'value 3' }], + columns: ['field1', 'field2', 'field3'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 100); + columnsController.columnOption(1, 'visibleWidth', 110); + columnsController.columnOption(2, 'visibleWidth', 120); + + columnsController.beginUpdate(); + columnsController.columnOption(1, 'caption', 'Updated field 2'); + columnsController.columnOption(0, 'visibleWidth', 105); + columnsController.columnOption(1, 'width', 150); + columnsController.endUpdate(); + + expect(columnsController.getColumns().map((column) => column.visibleWidth)).toEqual([ + 105, null, null, + ]); + }); + + it('should preserve visible widths that are pending for their respective columns', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1', field2: 'value 2', field3: 'value 3' }], + columns: ['field1', 'field2', 'field3'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 100); + columnsController.columnOption(1, 'visibleWidth', 110); + columnsController.columnOption(2, 'visibleWidth', 120); + + columnsController.beginUpdate(); + columnsController.columnOption(0, 'visibleWidth', 105); + columnsController.columnOption(1, 'visibleWidth', 115); + columnsController.columnOption(1, 'width', 150); + columnsController.endUpdate(); + + expect(columnsController.getColumns().map((column) => column.visibleWidth)).toEqual([ + 105, 115, null, + ]); + }); + + it('should clear pending visible widths after the update batch completes', async () => { + const { instance } = await createDataGrid({ + dataSource: [{ field1: 'value 1', field2: 'value 2', field3: 'value 3' }], + columns: ['field1', 'field2', 'field3'], + }); + const columnsController = instance.getController('columns'); + + columnsController.columnOption(0, 'visibleWidth', 100); + + columnsController.beginUpdate(); + columnsController.columnOption(0, 'visibleWidth', 105); + columnsController.columnOption(0, 'width', 150); + columnsController.endUpdate(); + + columnsController.columnOption(0, 'width', 160); + + expect(columnsController.columnOption(0, 'visibleWidth')).toBeNull(); + }); + }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index be977eb272a1..8380e15d8a89 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -90,6 +90,12 @@ interface IndexedColumns { negativeIndexedColumns: Record[]; } +export interface ColumnDimensionsUpdate { + columnIndex: Column['index']; + visibleWidth?: Column['visibleWidth'] | null; + width: Column['width']; +} + export class ColumnsController extends modules.Controller { public _skipProcessingColumnsChange: any; @@ -133,6 +139,8 @@ export class ColumnsController extends modules.Controller { public _columnChanges?: ColumnsChanges; + public _pendingVisibleWidthColumnIndices?: Set; + protected _dataController!: DataController; protected _focusController!: FocusController; @@ -1488,6 +1496,41 @@ export class ColumnsController extends modules.Controller { return this._columns ? this._columns.length : 0; } + /** Applies dimensions already resolved by an internal layout operation. */ + public updateColumnDimensions(updates: ColumnDimensionsUpdate[]): void { + if (!updates.length) { + return; + } + + const columnsByIndex = new Map(); + + this._columns.concat(this._commandColumns).forEach((column: Column) => { + if (!columnsByIndex.has(column.index)) { + columnsByIndex.set(column.index, column); + } + }); + + this.beginUpdate(); + try { + updates.forEach((dimensions) => { + const column = columnsByIndex.get(dimensions.columnIndex); + + if (!column) { + return; + } + + if (Object.prototype.hasOwnProperty.call(dimensions, 'visibleWidth')) { + columnOptionCore(this, column, 'visibleWidth', dimensions.visibleWidth); + } + columnOptionCore(this, column, 'width', dimensions.width, { + invalidateVisibleWidths: false, + }); + }); + } finally { + this.endUpdate(); + } + } + public columnOption(identifier, option?, value?, notFireEvent?) { const that = this; const columns = that._columns.concat(that._commandColumns); @@ -1501,10 +1544,10 @@ export class ColumnsController extends modules.Controller { if (arguments.length === 2) { return columnOptionCore(that, column, option); } - columnOptionCore(that, column, option, value, notFireEvent); + columnOptionCore(that, column, option, value, { notFireEvent }); } else if (isObject(option)) { each(option, (optionName, optionValue) => { - columnOptionCore(that, column, optionName, optionValue, notFireEvent); + columnOptionCore(that, column, optionName, optionValue, { notFireEvent }); }); } 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 d3dcd37385d2..a754b2e5bdb7 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 @@ -657,6 +657,10 @@ export const updateColumnChanges = ( }; export const fireColumnsChanged = function (that: ColumnsController) { + if (!that._updateLockCount) { + that._pendingVisibleWidthColumnIndices = undefined; + } + const onColumnsChanging: any = that.option('onColumnsChanging'); const columnChanges = that._columnChanges; const reinitOptionNames = ['dataField', 'lookup', 'dataType', 'columns']; @@ -715,9 +719,54 @@ export const fireOptionChanged = function (that: ColumnsController, options) { } }; -export const columnOptionCore = function (that: ColumnsController, column, optionName, value?, notFireEvent?) { +const trackPendingVisibleWidthChange = (that: ColumnsController, columnIndex): void => { + if (isDefined(columnIndex)) { + that._pendingVisibleWidthColumnIndices ??= new Set(); + that._pendingVisibleWidthColumnIndices.add(columnIndex); + } +}; + +const isVisibleWidthChangePendingForColumn = (that: ColumnsController, columnIndex): boolean => !!that._pendingVisibleWidthColumnIndices?.has(columnIndex); + +const invalidateStaleVisibleWidths = (that: ColumnsController, changedColumn): void => { + const hasCalculatedVisibleWidth = isNumeric(changedColumn.visibleWidth) + || changedColumn.visibleWidth === 'auto'; + const shouldInvalidateChangedColumnVisibleWidth = hasCalculatedVisibleWidth + && !isVisibleWidthChangePendingForColumn(that, changedColumn.index); + + if (shouldInvalidateChangedColumnVisibleWidth) { + changedColumn.visibleWidth = null; + } + + that._columns.concat(that._commandColumns).forEach((column) => { + const hasCalculatedVisibleWidth = isNumeric(column.visibleWidth) + && (!isDefined(column.width) || column.width === 'auto'); + const shouldInvalidateVisibleWidth = column !== changedColumn + && hasCalculatedVisibleWidth + && !isVisibleWidthChangePendingForColumn(that, column.index); + + if (shouldInvalidateVisibleWidth) { + column.visibleWidth = null; + } + }); +}; + +interface ColumnOptionCoreOptions { + invalidateVisibleWidths?: boolean; + notFireEvent?: boolean; +} + +export const columnOptionCore = function ( + that: ColumnsController, + column, + optionName, + value?, + options: ColumnOptionCoreOptions = {}, +) { const optionGetter = compileGetter(optionName); const columnIndex = column.index; + const { invalidateVisibleWidths = true } = options; + let { notFireEvent } = options; let columns; let changeType; let initialColumn; @@ -740,6 +789,12 @@ export const columnOptionCore = function (that: ColumnsController, column, optio changeType = 'columns'; } + if (optionName === 'visibleWidth') { + trackPendingVisibleWidthChange(that, columnIndex); + } else if (optionName === 'width' && invalidateVisibleWidths) { + invalidateStaleVisibleWidths(that, column); + } + const optionSetter = compileSetter(optionName); // @ts-expect-error optionSetter(column, value, { functionsAsIs: true }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts index e41d7c631827..935dd03bd496 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts @@ -29,7 +29,7 @@ import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import type { ColumnChooserView } from '../column_chooser/m_column_chooser'; import type { ColumnHeadersView } from '../column_headers/m_column_headers'; -import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import type { ColumnDimensionsUpdate, ColumnsController } from '../columns_controller/m_columns_controller'; import type { HeaderPanel } from '../header_panel/m_header_panel'; import modules from '../m_modules'; import gridCoreUtils from '../m_utils'; @@ -1081,21 +1081,23 @@ export class ColumnsResizerViewController extends modules.ViewController { return isString(width) && width.endsWith('%'); } - function setColumnWidth(column, columnWidth, contentWidth, adaptColumnWidthByRatio) { + function getColumnDimensions(column, columnWidth, contentWidth, adaptColumnWidthByRatio): ColumnDimensionsUpdate | undefined { if (column) { const oldColumnWidth = column.width; if (oldColumnWidth) { adaptColumnWidthByRatio = isPercentWidth(oldColumnWidth); } - if (adaptColumnWidthByRatio) { - columnsController.columnOption(column.index, 'visibleWidth', columnWidth); - columnsController.columnOption(column.index, 'width', `${(columnWidth / contentWidth * 100).toFixed(3)}%`); - } else { - columnsController.columnOption(column.index, 'visibleWidth', null); - columnsController.columnOption(column.index, 'width', columnWidth); - } + return { + columnIndex: column.index, + visibleWidth: adaptColumnWidthByRatio ? columnWidth : null, + width: adaptColumnWidthByRatio + ? `${(columnWidth / contentWidth * 100).toFixed(3)}%` + : columnWidth, + }; } + + return undefined; } function correctContentWidth(contentWidth, visibleColumns) { @@ -1168,12 +1170,19 @@ export class ColumnsResizerViewController extends modules.ViewController { contentWidth = correctContentWidth(contentWidth, visibleColumns); - setColumnWidth(column, cellWidth, contentWidth, adaptColumnWidthByRatio); + const columnDimensions = getColumnDimensions(column, cellWidth, contentWidth, adaptColumnWidthByRatio); if (isNextColumnMode) { nextCellWidth = Math.floor(nextCellWidth); - setColumnWidth(nextColumn, nextCellWidth, contentWidth, adaptColumnWidthByRatio); + const nextColumnDimensions = getColumnDimensions(nextColumn, nextCellWidth, contentWidth, adaptColumnWidthByRatio); + const updates = [columnDimensions, nextColumnDimensions].filter(isDefined); + + columnsController.updateColumnDimensions(updates); } else { + if (columnDimensions) { + columnsController.updateColumnDimensions([columnDimensions]); + } + const columnWidths = this._columnHeadersView.getColumnWidths(); columnWidths[resizingInfo.currentColumnIndex] = cellWidth; const hasScroll = columnWidths.reduce((totalWidth, width) => totalWidth + width, 0) > this._rowsView.contentWidth(); @@ -1183,11 +1192,19 @@ export class ColumnsResizerViewController extends modules.ViewController { columnsController.columnOption(visibleColumns[lastColumnIndex].index, 'visibleWidth', 'auto'); } } + + const updates: ColumnDimensionsUpdate[] = []; + for (let i = 0; i < columnWidths.length; i++) { if (visibleColumns[i] && visibleColumns[i] !== column && visibleColumns[i].width === undefined) { - columnsController.columnOption(visibleColumns[i].index, 'width', columnWidths[i]); + updates.push({ + columnIndex: visibleColumns[i].index, + width: columnWidths[i], + }); } } + + columnsController.updateColumnDimensions(updates); } columnsController.endUpdate(); diff --git a/packages/devextreme/testing/helpers/gridBaseMocks.js b/packages/devextreme/testing/helpers/gridBaseMocks.js index 029e59537099..7238f6af1b81 100644 --- a/packages/devextreme/testing/helpers/gridBaseMocks.js +++ b/packages/devextreme/testing/helpers/gridBaseMocks.js @@ -582,6 +582,15 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo } }, + updateColumnDimensions: function(updates) { + updates.forEach((dimensions) => { + if(Object.prototype.hasOwnProperty.call(dimensions, 'visibleWidth')) { + this.columnOption(dimensions.columnIndex, 'visibleWidth', dimensions.visibleWidth); + } + this.columnOption(dimensions.columnIndex, 'width', dimensions.width); + }); + }, + addCommandColumn: function(commandColumn) { columns && columns.forEach(function(column, index) { if(column.command === commandColumn.command) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js index f37b9373fc24..9797b25d2c67 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js @@ -4955,7 +4955,6 @@ QUnit.module('Move Columns', { beforeEach: setupModule, afterEach: teardownModul }); QUnit.module('Column Option', { beforeEach: setupModule, afterEach: teardownModule }, () => { - QUnit.test('update exist column parameter', function(assert) { this.applyOptions({ columns: ['field1', 'field2', 'field3'] });