Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9771eca
Refactor _synchronizeColumns method
nightskylark Jul 15, 2026
3d602d3
fix(popover): remove unnecessary type assertion for overlay stack check
nightskylark Jul 15, 2026
9957424
fix(m_utils): handle non-numeric selectionStart and selectionEnd values
nightskylark Jul 15, 2026
32ad957
fix(grid_view): correct spacing in _synchronizeColumns method
nightskylark Jul 15, 2026
dfde56e
fix(m_utils): remove selectionDirection from SelectionRange interface…
nightskylark Jul 21, 2026
ab0d0ef
Merge branch 'main' into T1329677
nightskylark Jul 21, 2026
c3b08be
Merge branch 'main' into T1329677
nightskylark Jul 22, 2026
304db48
Merge branch 'main' into T1329677
nightskylark Jul 24, 2026
7cb4328
Merge branch 'main' into T1329677
nightskylark Jul 29, 2026
99c13db
Merge branch 'main' into T1329677
nightskylark Aug 4, 2026
f73f438
Merge branch 'main' into T1329677
nightskylark Aug 18, 2026
a0ab2ec
Merge branch 'main' into T1329677
nightskylark Sep 22, 2026
781388e
Update after review
nightskylark Sep 22, 2026
fb7512e
Merge branch 'main' into T1329677
nightskylark Sep 22, 2026
1312c25
refactor(ResizingController): simplify expand column width calculation
nightskylark Sep 22, 2026
e86dc6d
refactor(ResizingController): remove redundant comment before measure…
nightskylark Sep 22, 2026
57b55a3
feat(SelectionRange): add SelectionRange interface and update imports
nightskylark Sep 23, 2026
9f83616
refactor(ResizingController): rename private methods for consistency
nightskylark Sep 23, 2026
cdce32f
refactor(ResizingController): rename private method _setMaxWidth to s…
nightskylark Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions packages/devextreme/js/__internal/grids/grid_core/m_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export function isDateType(dataType: string | undefined): boolean {
return dataType === 'date' || dataType === 'datetime';
}

export interface SelectionRange {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually keep types and interfaces in a separate types.ts file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

selectionStart: number;
selectionEnd: number;
}

const getIntervalSelector = function () {
const data = arguments[1];
const value = this.calculateCellValue(data);
Expand Down Expand Up @@ -504,22 +509,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,
};
Comment thread
nightskylark marked this conversation as resolved.
}
} 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 */ }
Expand Down
Original file line number Diff line number Diff line change
@@ -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]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ 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';
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';
Expand All @@ -37,6 +39,8 @@ const GROUP_ROW_SELECTOR = 'tr.dx-group-row';

const HIDDEN_COLUMNS_WIDTH = 'adaptiveHidden';

type ColumnWidth = number | string | undefined;
Comment thread
nightskylark marked this conversation as resolved.
Outdated

const VIEW_NAMES = [
'columnsSeparatorView',
'blockSeparatorView',
Expand Down Expand Up @@ -81,7 +85,7 @@ 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);
};
Expand All @@ -105,8 +109,6 @@ export class ResizingController extends modules.ViewController {

private _prevContentMinHeight: any;

private _maxWidth: any;

private _hasWidth: any;

private _hasHeight: any;
Expand All @@ -127,6 +129,8 @@ export class ResizingController extends modules.ViewController {

public resizeCompleted!: Callback;

private _isMaxWidthSet = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: An underscore in the name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


protected callbackNames() {
return ['resizeCompleted'];
}
Expand Down Expand Up @@ -345,85 +349,74 @@ 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 _setMaxWidth(value: number): void {
this._isMaxWidthSet = true;
this.component.$element().css('maxWidth', value);
}

const normalizeWidthsByExpandColumns = function () {
let expandColumnWidth;
private _clearMaxWidth(): void {
if (!this._isMaxWidthSet) {
return;
}

each(visibleColumns, (index, column) => {
if (column.type === 'groupExpand') {
expandColumnWidth = resultWidths[index];
}
});
this._isMaxWidthSet = false;

each(visibleColumns, (index, column) => {
if (column.type === 'groupExpand' && expandColumnWidth) {
resultWidths[index] = expandColumnWidth;
}
});
};
const element = this.component.$element().get(0) as HTMLElement | undefined;

!needBestFit && each(visibleColumns, (index, column) => {
if (column.width === 'auto') {
needBestFit = true;
return false;
}
return undefined;
});
if (element) {
element.style.maxWidth = '';
}
}

each(visibleColumns, (index, column) => {
if (column.minWidth) {
hasMinWidth = true;
return false;
}
return undefined;
});
private _enableTemporaryBestFitMode(): () => void {
const $element = this.component.$element();
const focusedElement = domAdapter.getActiveElement($element.get(0) as HTMLElement | null);
const selectionRange = gridCoreUtils.getSelectionRange(focusedElement);

this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354
this._toggleBestFitMode(true);

this._setVisibleWidths(visibleColumns, []);
return (): void => {
this._toggleBestFitMode(false);

const $element = this.component.$element();
if (focusedElement && focusedElement !== domAdapter.getActiveElement()) {
const isFocusOutsideWindow = getBoundingRect(focusedElement).bottom < 0;

if (needBestFit) {
// @ts-expect-error
focusedElement = domAdapter.getActiveElement($element.get(0));
selectionRange = gridCoreUtils.getSelectionRange(focusedElement);
this._toggleBestFitMode(true);
resetBestFitMode = true;
}
if (!isFocusOutsideWindow) {
restoreFocus(focusedElement, selectionRange);
}
}
};
}

if ($element && $element.get(0) && this._maxWidth) {
delete this._maxWidth;
$element[0].style.maxWidth = '';
}
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 restoreAfterBestFitMode = needBestFit && this._enableTemporaryBestFitMode();
this._clearMaxWidth();

// eslint-disable-next-line @typescript-eslint/no-floating-promises
deferUpdate(() => {
if (needBestFit) {
let resultWidths: ColumnWidth[] = [];

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;
Expand All @@ -433,21 +426,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);
}
Expand Down Expand Up @@ -485,6 +471,34 @@ export class ResizingController extends modules.ViewController {
return freeWidth / columnCountWithoutWidth;
}

private _normalizeWidthsByExpandColumns(
resultWidths: ColumnWidth[],
visibleColumns: Column[],
): void {
const isExpandColumn = (column: Column): boolean => column.type === GROUP_COMMAND_COLUMN_NAME;

const lastExpandColumnIndex = visibleColumns.reduce(
(result, column, index) => (isExpandColumn(column) ? index : result),
-1,
);

if (lastExpandColumnIndex < 0) {
return;
}

const expandColumnWidth = resultWidths[lastExpandColumnIndex];

if (!expandColumnWidth) {
return;
}

visibleColumns.forEach((column, index) => {
if (isExpandColumn(column)) {
resultWidths[index] = expandColumnWidth;
}
});
}

/**
* @extended: adaptivity
*/
Expand All @@ -494,7 +508,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++) {
Expand Down Expand Up @@ -547,9 +560,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._setMaxWidth(totalWidth + scrollbarWidth + borderWidth);
}
}
}
Expand Down
Loading