From ee1646e2adfa08317e9a4831ce55b7a3565d6a26 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:12:18 +0200 Subject: [PATCH 1/5] Grids: type m_grid_view --- .../master_detail/m_master_detail.ts | 2 + .../grids/grid_core/views/m_grid_view.ts | 428 ++++++++++-------- 2 files changed, 240 insertions(+), 190 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts index 51699abd7b39..8db2152649e5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts @@ -317,6 +317,8 @@ const resizing = (Base: ModuleType) => class ResizingMasterD const isDetailGridWidthChanged = isDetailHorizontalScrollCanBeShown && detailElementWidth !== getWidth($detailElement); if (isDetailHorizontalScrollCanBeShown && isDetailGridWidthChanged) { + // @ts-expect-error updateDimensions() returns undefined only for an invisible grid + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result this.updateDimensions().done(() => d.resolve(true)); } else { d.resolve(true); 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 acad2f16ab18..34605ef6f70b 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 @@ -29,10 +29,13 @@ import type { DataChange } from '../data_controller/types'; import type { DataSourceController } from '../data_source/data_source_controller'; import gridCoreUtils from '../m_utils'; import modules from '../modules/modules'; -import type { SelectionRange } from '../types'; +import type { + InternalGridOptions, OptionChanged, SelectionRange, Views, +} from '../types'; import { CLASSES } from './const'; +import type { ColumnsView } from './m_columns_view'; import type { RowsView } from './m_rows_view'; -import type { ColumnWidth } from './types'; +import type { ColumnWidth, ScrollPosition, ViewDataChange } from './types'; const BORDERS_CLASS = 'borders'; const IMPORTANT_MARGIN_CLASS = 'important-margin'; @@ -65,23 +68,36 @@ const E2E_ATTRIBUTES = { a11yStatusContainer: 'e2e-a11y-general-status-container', }; -const isPercentWidth = function (width) { +const isPercentWidth = function isPercentWidth(width: ColumnWidth): boolean { return isString(width) && width.endsWith('%'); }; -const isPixelWidth = function (width) { +const isPixelWidth = function isPixelWidth(width: ColumnWidth): boolean { return isString(width) && width.endsWith('px'); }; -const calculateFreeWidth = function (that, widths) { +const calculateFreeWidth = function calculateFreeWidth( + that: ResizingController, + widths: ColumnWidth[], +): number { + // @ts-expect-error the helper reads non-public members of the controller const contentWidth = that._rowsView.contentWidth(); + // @ts-expect-error the helper reads non-public members of the controller const totalWidth = that._getTotalWidth(widths, contentWidth); return contentWidth - totalWidth; }; -const calculateFreeWidthWithCurrentMinWidth = function (that, columnIndex, currentMinWidth, widths) { - return calculateFreeWidth(that, widths.map((width, index) => (index === columnIndex ? currentMinWidth : width))); +const calculateFreeWidthWithCurrentMinWidth = function calculateFreeWidthWithCurrentMinWidth( + that: ResizingController, + columnIndex: number, + currentMinWidth: number, + widths: ColumnWidth[], +): number { + return calculateFreeWidth( + that, + widths.map((width, index) => (index === columnIndex ? currentMinWidth : width)), + ); }; const restoreFocus = (focusedElement: Element, selectionRange: SelectionRange): void => { @@ -90,7 +106,7 @@ const restoreFocus = (focusedElement: Element, selectionRange: SelectionRange): }; export class ResizingController extends modules.ViewController { - private _refreshSizesHandler: any; + private _refreshSizesHandler?: (change: DataChange) => void; public _dataController!: DataController; @@ -106,35 +122,35 @@ export class ResizingController extends modules.ViewController { private _gridView!: GridView; - private _prevContentMinHeight: any; + private _prevContentMinHeight!: string | null; - private _hasWidth: any; + private _hasWidth?: boolean; - private _hasHeight: any; + private _hasHeight?: boolean; - private _resizeDeferred: any; + private _resizeDeferred?: DeferredObj; - public _lastWidth: any; + public _lastWidth?: number; - private _devicePixelRatio: any; + private _devicePixelRatio?: number; - private _lastHeight: any; + private _lastHeight?: number; protected adaptiveColumnsController!: AdaptiveColumnsController; private _editorFactoryController!: EditorFactory; - protected _updateScrollableTimeoutID: any; + protected _updateScrollableTimeoutID?: ReturnType; public resizeCompleted!: Callback; private isMaxWidthSet = false; - protected callbackNames() { + protected callbackNames(): string[] { return ['resizeCompleted']; } - public init() { + public init(): void { this._prevContentMinHeight = null; this._dataController = this.getController('data'); this.dataSourceController = this.getController('dataSource'); @@ -147,11 +163,10 @@ export class ResizingController extends modules.ViewController { this._gridView = this.getView('gridView'); } - private _initPostRenderHandlers() { + private _initPostRenderHandlers(): void { if (!this._refreshSizesHandler) { - this._refreshSizesHandler = (change: DataChange) => { - // @ts-expect-error - let resizeDeferred = new Deferred().resolve(null); + const refreshSizesHandler = (change: DataChange): void => { + let resizeDeferred: DeferredObj = Deferred().resolve(null); const changeType = change?.changeType; // @ts-expect-error e.isDelayed is set for virtual scrolling with scrolling.legacyMode const isDelayed = change?.isDelayed; @@ -161,7 +176,7 @@ export class ResizingController extends modules.ViewController { && changeType !== 'pageIndex' && !isDelayed; - this._dataController.changed.remove(this._refreshSizesHandler); + this._dataController.changed.remove(refreshSizesHandler); if (this._checkSize()) { resizeDeferred = this._refreshSizes(change); @@ -174,18 +189,20 @@ export class ResizingController extends modules.ViewController { }); } }; + this._refreshSizesHandler = refreshSizesHandler; // TODO remove resubscribing this._dataController.changed.add(() => { - this._dataController.changed.add(this._refreshSizesHandler); + this._dataController.changed.add(refreshSizesHandler); }); } } - private _refreshSizes(e) { + private _refreshSizes(e: ViewDataChange): DeferredObj { const changeType = e?.changeType; + // @ts-expect-error e.isDelayed is set for virtual scrolling with scrolling.legacyMode const isDelayed = e?.isDelayed; - if (!e || ['refresh', 'prepend', 'append'].includes(changeType)) { + if (!e || (changeType !== undefined && ['refresh', 'prepend', 'append'].includes(changeType))) { if (!isDelayed) { return this.resize(); } @@ -193,8 +210,7 @@ export class ResizingController extends modules.ViewController { if (changeType === 'update') { if (!e.changeTypes?.length) { - // @ts-expect-error - return new Deferred().resolve(null); + return Deferred().resolve(null); } const items = this._dataController.items(); @@ -202,15 +218,16 @@ export class ResizingController extends modules.ViewController { const isShowingNoDataPanel = items.length === 0 && e.changeTypes[0] === 'remove'; if (!isHidingNoDataPanel && !isShowingNoDataPanel && !e.needUpdateDimensions) { - // @ts-expect-error - const deferred = new Deferred(); + const deferred = Deferred(); this._waitAsyncTemplates().done(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferUpdate(() => deferRender(() => deferUpdate(() => { this._setScrollerSpacing(); this._rowsView.resize(); deferred.resolve(); }))); + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result }).fail(deferred.reject); return deferred; @@ -219,26 +236,27 @@ export class ResizingController extends modules.ViewController { return this.resize(); } - // @ts-expect-error - return new Deferred().resolve(null); + return Deferred().resolve(null); } /** * @extended: master_detail */ - public fireContentReadyAction() { + public fireContentReadyAction(): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget this.component._fireContentReadyAction(); } - protected _getWidgetAriaLabel() { + protected _getWidgetAriaLabel(): string { return 'dxDataGrid-ariaDataGrid'; } - private _setAriaLabel(e?: any): void { + private _setAriaLabel(e?: DataChange): void { let widgetStatusText = ''; let labelParts: string[] = []; - const columnCount = this._columnsController?._columns?.filter(({ visible }) => !!visible).length ?? 0; + const columnCount = this._columnsController?._columns + ?.filter(({ visible }) => !!visible).length ?? 0; const totalItemsCount = Math.max(0, this.dataSourceController.totalItemsCount()); const widgetAriaLabel = this._getWidgetAriaLabel(); widgetStatusText = messageLocalization @@ -260,7 +278,7 @@ export class ResizingController extends modules.ViewController { } } - private _getBestFitWidths() { + private _getBestFitWidths(): number[] { const rowsView = this._rowsView; const columnHeadersView = this._columnHeadersView; let widths = rowsView.getColumnWidths(); @@ -275,7 +293,7 @@ export class ResizingController extends modules.ViewController { return widths; } - private _setVisibleWidths(visibleColumns, widths) { + private _setVisibleWidths(visibleColumns: Column[], widths: ColumnWidth[]): void { const columnsController = this._columnsController; columnsController.beginUpdate(); each(visibleColumns, (index, column) => { @@ -285,18 +303,25 @@ export class ResizingController extends modules.ViewController { columnsController.endUpdate(); } - private _toggleBestFitModeForView(view, className, isBestFit) { - if (!view || !view.isVisible()) return; + private _toggleBestFitModeForView( + view: ColumnsView | undefined, + className: string, + isBestFit: boolean, + ): void { + if (!view?.isVisible()) { + return; + } const $rowsTables = this._rowsView.getTableElements(); const $viewTables = view.getTableElements(); each($rowsTables, (index, tableElement) => { - let $tableBody; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the if/else + let $tableBody: dxElementWrapper; const $rowsTable = $(tableElement); const $viewTable = $viewTables.eq(index); - if ($viewTable && $viewTable.length) { + if ($viewTable?.length) { if (isBestFit) { $tableBody = $viewTable.children('tbody').appendTo($rowsTable); } else { @@ -311,7 +336,7 @@ export class ResizingController extends modules.ViewController { /** * @extended: adaptivity, master_detail */ - protected _toggleBestFitMode(isBestFit) { + protected _toggleBestFitMode(isBestFit: boolean): void { const $rowsTable = this._rowsView.getTableElement(); const $rowsFixedTable = this._rowsView.getTableElements().eq(1); @@ -336,7 +361,7 @@ export class ResizingController extends modules.ViewController { } } - private _toggleContentMinHeight(value) { + private _toggleContentMinHeight(value: boolean | undefined): void { const $contentElement = this._rowsView._findContentElement(); if (value === true) { @@ -344,7 +369,11 @@ export class ResizingController extends modules.ViewController { } if (isDefined(this._prevContentMinHeight)) { - $contentElement.css({ minHeight: value ? gridCoreUtils.getContentHeightLimit(browser) : this._prevContentMinHeight }); + $contentElement.css({ + minHeight: value + ? gridCoreUtils.getContentHeightLimit(browser) + : this._prevContentMinHeight, + }); } } @@ -392,7 +421,7 @@ export class ResizingController extends modules.ViewController { 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 needBestFit = !!this._needBestFit() || visibleColumns.some((column) => column.width === 'auto'); const hasMinWidth = visibleColumns.some((column) => !!column.minWidth); this._toggleContentMinHeight(this._hasHeight); // T1047239, T1270354 @@ -453,18 +482,18 @@ export class ResizingController extends modules.ViewController { /** * @extended: adaptivity */ - protected _needBestFit() { + protected _needBestFit(): boolean | undefined { return this.option('columnAutoWidth'); } /** * @extended: adaptivity */ - protected _needStretch() { + protected _needStretch(): boolean { return this._columnsController.getVisibleColumns().some((c) => c.width === 'auto' && !c.command); } - private _getAverageColumnsWidth(resultWidths) { + private _getAverageColumnsWidth(resultWidths: ColumnWidth[]): number { const freeWidth = calculateFreeWidth(this, resultWidths); const columnCountWithoutWidth = resultWidths.filter((width) => width === undefined).length; @@ -502,35 +531,41 @@ export class ResizingController extends modules.ViewController { /** * @extended: adaptivity */ - protected _correctColumnWidths(resultWidths, visibleColumns) { - const that = this; - let i; + protected _correctColumnWidths(resultWidths: ColumnWidth[], visibleColumns: Column[]): boolean { let hasPercentWidth = false; let hasAutoWidth = false; let isColumnWidthsCorrected = false; - const hasWidth = that._hasWidth; + const hasWidth = this._hasWidth; - for (i = 0; i < visibleColumns.length; i++) { + for (let i = 0; i < visibleColumns.length; i += 1) { const index = i; const column = visibleColumns[index]; const isHiddenColumn = resultWidths[index] === HIDDEN_COLUMNS_WIDTH; let width = resultWidths[index]; const { minWidth } = column; - if (minWidth) { - if (width === undefined) { - const averageColumnsWidth = that._getAverageColumnsWidth(resultWidths); - width = averageColumnsWidth; - } else if (isPercentWidth(width)) { - const freeWidth = calculateFreeWidthWithCurrentMinWidth(that, index, minWidth, resultWidths); - - if (freeWidth < 0) { - width = -1; - } + if (minWidth && width === undefined) { + const averageColumnsWidth = this._getAverageColumnsWidth(resultWidths); + width = averageColumnsWidth; + } else if (minWidth && isPercentWidth(width)) { + const freeWidth = calculateFreeWidthWithCurrentMinWidth( + this, + index, + minWidth, + resultWidths, + ); + + if (freeWidth < 0) { + width = -1; } } - const realColumnWidth = that._getRealColumnWidth(index, resultWidths.map((columnWidth, columnIndex) => (index === columnIndex ? width : columnWidth))); + const realColumnWidth = this._getRealColumnWidth( + index, + resultWidths.map( + (columnWidth, columnIndex) => (index === columnIndex ? width : columnWidth), + ), + ); if (minWidth && !isHiddenColumn && realColumnWidth < minWidth) { resultWidths[index] = minWidth; @@ -546,13 +581,16 @@ export class ResizingController extends modules.ViewController { } if (!hasAutoWidth && resultWidths.length) { - const $rowsViewElement = that._rowsView.element(); - const contentWidth = that._rowsView.contentWidth(); - const scrollbarWidth = that._rowsView.getScrollbarWidth(); - const totalWidth = that._getTotalWidth(resultWidths, contentWidth); + const $rowsViewElement = this._rowsView.element(); + const contentWidth = this._rowsView.contentWidth(); + const scrollbarWidth = this._rowsView.getScrollbarWidth(); + const totalWidth = this._getTotalWidth(resultWidths, contentWidth); if (totalWidth < contentWidth) { - const lastColumnIndex = gridCoreUtils.getLastResizableColumnIndex(visibleColumns, resultWidths); + const lastColumnIndex = gridCoreUtils.getLastResizableColumnIndex( + visibleColumns, + resultWidths, + ); if (lastColumnIndex >= 0) { resultWidths[lastColumnIndex] = 'auto'; @@ -560,7 +598,7 @@ export class ResizingController extends modules.ViewController { if (hasWidth === false && !hasPercentWidth) { const borderWidth = gridCoreUtils.getComponentBorderWidth(this, $rowsViewElement); - that.setMaxWidth(totalWidth + scrollbarWidth + borderWidth); + this.setMaxWidth(totalWidth + scrollbarWidth + borderWidth); } } } @@ -568,17 +606,17 @@ export class ResizingController extends modules.ViewController { return isColumnWidthsCorrected; } - private _processStretch(resultSizes, visibleColumns) { + private _processStretch(resultSizes: ColumnWidth[], visibleColumns: Column[]): void { const groupSize = this._rowsView.contentWidth(); const tableSize = this._getTotalWidth(resultSizes, groupSize); const unusedIndexes = { length: 0 }; if (!resultSizes.length) return; - each(visibleColumns, function (index) { + each(visibleColumns, function markUnusedIndex(this: Column, index: number) { if (this.width || resultSizes[index] === HIDDEN_COLUMNS_WIDTH) { unusedIndexes[index] = true; - unusedIndexes.length++; + unusedIndexes.length += 1; } }); @@ -586,64 +624,69 @@ export class ResizingController extends modules.ViewController { const diffElement = Math.floor(diff / (resultSizes.length - unusedIndexes.length)); let onePixelElementsCount = diff - diffElement * (resultSizes.length - unusedIndexes.length); if (diff >= 0) { - for (let i = 0; i < resultSizes.length; i++) { + for (let i = 0; i < resultSizes.length; i += 1) { if (unusedIndexes[i]) { + // eslint-disable-next-line no-continue -- an inverted if would exceed max-depth continue; } + // @ts-expect-error columns without a width have best-fit widths here resultSizes[i] += diffElement; - if (onePixelElementsCount > 0) { - if (onePixelElementsCount < 1) { - resultSizes[i] += onePixelElementsCount; - onePixelElementsCount = 0; - } else { - resultSizes[i]++; - onePixelElementsCount--; - } + if (onePixelElementsCount > 0 && onePixelElementsCount < 1) { + // @ts-expect-error columns without a width have best-fit widths here + resultSizes[i] += onePixelElementsCount; + onePixelElementsCount = 0; + } else if (onePixelElementsCount > 0) { + resultSizes[i] = Number(resultSizes[i]) + 1; + onePixelElementsCount -= 1; } } } } - private _getRealColumnWidth(columnIndex, columnWidths, groupWidth?) { + private _getRealColumnWidth( + columnIndex: number, + columnWidths: ColumnWidth[], + groupWidth?: number, + ): number { let ratio = 1; const width = columnWidths[columnIndex]; if (!isPercentWidth(width)) { - return parseFloat(width); + return parseFloat(String(width)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - const percentTotalWidth = columnWidths.reduce((sum, width, index) => { - if (!isPercentWidth(width)) { + const percentTotalWidth = columnWidths.reduce((sum, columnWidth, index) => { + if (!isPercentWidth(columnWidth)) { return sum; } - return sum + parseFloat(width); + return sum + parseFloat(String(columnWidth)); }, 0); - const pixelTotalWidth = columnWidths.reduce((sum, width) => { - if (!width || width === HIDDEN_COLUMNS_WIDTH || isPercentWidth(width)) { + const pixelTotalWidth = columnWidths.reduce((sum, columnWidth) => { + if (!columnWidth || columnWidth === HIDDEN_COLUMNS_WIDTH || isPercentWidth(columnWidth)) { return sum; } - return sum + parseFloat(width); + return sum + parseFloat(String(columnWidth)); }, 0); - groupWidth = groupWidth || this._rowsView.contentWidth(); + const currentGroupWidth = groupWidth || this._rowsView.contentWidth(); - const freeSpace = groupWidth - pixelTotalWidth; - const percentTotalWidthInPixel = percentTotalWidth * groupWidth / 100; + const freeSpace = currentGroupWidth - pixelTotalWidth; + const percentTotalWidthInPixel = (percentTotalWidth * currentGroupWidth) / 100; - if (pixelTotalWidth > 0 && (percentTotalWidthInPixel + pixelTotalWidth) >= groupWidth) { + if (pixelTotalWidth > 0 && (percentTotalWidthInPixel + pixelTotalWidth) >= currentGroupWidth) { ratio = percentTotalWidthInPixel > freeSpace ? freeSpace / percentTotalWidthInPixel : 1; } - return parseFloat(width) * groupWidth * ratio / 100; + return (parseFloat(String(width)) * currentGroupWidth * ratio) / 100; } - private _getTotalWidth(widths, groupWidth) { + private _getTotalWidth(widths: ColumnWidth[], groupWidth: number): number { let result = 0; - for (let i = 0; i < widths.length; i++) { + for (let i = 0; i < widths.length; i += 1) { const width = widths[i]; if (width && width !== HIDDEN_COLUMNS_WIDTH) { result += this._getRealColumnWidth(i, widths, groupWidth); @@ -653,27 +696,26 @@ export class ResizingController extends modules.ViewController { return Math.ceil(result); } - private _getGroupElement() { - return this.component.$element().children().get(0); + private _getGroupElement(): HTMLElement | undefined { + return this.component.$element().children().get(0) as HTMLElement | undefined; } - public updateSize(rootElement) { - const that = this; + public updateSize(rootElement: dxElementWrapper): void { const $rootElement = $(rootElement); - const importantMarginClass = that.addWidgetPrefix(IMPORTANT_MARGIN_CLASS); + const importantMarginClass = this.addWidgetPrefix(IMPORTANT_MARGIN_CLASS); - if (that._hasHeight === undefined && $rootElement && $rootElement.is(':visible') && getWidth($rootElement)) { - const $groupElement = $rootElement.children(`.${that.getWidgetContainerClass()}`); + if (this._hasHeight === undefined && $rootElement && $rootElement.is(':visible') && getWidth($rootElement)) { + const $groupElement = $rootElement.children(`.${this.getWidgetContainerClass()}`); if ($groupElement.length) { $groupElement.detach(); } - that._hasHeight = !!getHeight($rootElement); + this._hasHeight = !!getHeight($rootElement); const width = getWidth($rootElement); $rootElement.addClass(importantMarginClass); - that._hasWidth = getWidth($rootElement) === width; + this._hasWidth = getWidth($rootElement) === width; $rootElement.removeClass(importantMarginClass); if ($groupElement.length) { @@ -682,11 +724,11 @@ export class ResizingController extends modules.ViewController { } } - public publicMethods() { + public publicMethods(): string[] { return ['resize', 'updateDimensions']; } - private _waitAsyncTemplates() { + private _waitAsyncTemplates(): DeferredObj { return when( this._columnHeadersView?.waitAsyncTemplates(true), this._rowsView?.waitAsyncTemplates(true), @@ -699,50 +741,55 @@ export class ResizingController extends modules.ViewController { */ public resize(): DeferredObj { if (this.component._requireResize) { - // @ts-expect-error - return new Deferred().resolve(); + return Deferred().resolve(); } - // @ts-expect-error - const d = new Deferred(); + const d = Deferred(); this._waitAsyncTemplates().done(() => { when(this.updateDimensions()) + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result .done(d.resolve) + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result .fail(d.reject); + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- ignores the result }).fail(d.reject); - return d.promise().done(() => { + // @ts-expect-error promise() is typed as Promise but returns a Deferred-like value at runtime + const promise: DeferredObj = d.promise(); + + return promise.done(() => { this.resizeCompleted.fire(); }); } - public updateDimensions(checkSize?) { - const that = this; - - that._initPostRenderHandlers(); + public updateDimensions(checkSize?: boolean): DeferredObj | undefined { + this._initPostRenderHandlers(); // T335767 - if (!that._checkSize(checkSize)) { - return; + if (!this._checkSize(checkSize)) { + return undefined; } - const prevResult = that._resizeDeferred; - // @ts-expect-error - const result = that._resizeDeferred = new Deferred(); + const prevResult = this._resizeDeferred; + const result = Deferred(); + this._resizeDeferred = result; when(prevResult).always(() => { deferRender(() => { - if (that._dataController.isLoaded()) { - that.synchronizeColumns(); + if (this._dataController.isLoaded()) { + this.synchronizeColumns(); } // IE11 - that._resetGroupElementHeight(); + this._resetGroupElementHeight(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferUpdate(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferRender(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferUpdate(() => { - that._updateDimensionsCore(); + this._updateDimensionsCore(); }); }); }); @@ -750,20 +797,19 @@ export class ResizingController extends modules.ViewController { }).done(result.resolve).fail(result.reject); }); + // @ts-expect-error promise() is typed as Promise but returns a Deferred-like value at runtime return result.promise(); } - private _resetGroupElementHeight() { + private _resetGroupElementHeight(): void { const groupElement = this._getGroupElement(); const scrollable = this._rowsView.getScrollable(); - // @ts-expect-error - if (groupElement && groupElement.style.height && (!scrollable || !scrollable.scrollTop())) { - // @ts-expect-error + if (groupElement?.style.height && !scrollable?.scrollTop()) { groupElement.style.height = ''; } } - private _checkSize(checkSize?) { + private _checkSize(checkSize?: boolean): boolean { const $rootElement = this.component.$element(); const isWidgetVisible = $rootElement.is(':visible'); const isGridSizeChanged = this._lastWidth !== getWidth($rootElement) @@ -773,25 +819,27 @@ export class ResizingController extends modules.ViewController { return isWidgetVisible && (!checkSize || isGridSizeChanged); } - private _setScrollerSpacingCore() { - const that = this; - const vScrollbarWidth = that._rowsView.getScrollbarWidth(); - const hScrollbarWidth = that._rowsView.getScrollbarWidth(true); + private _setScrollerSpacingCore(): void { + const vScrollbarWidth = this._rowsView.getScrollbarWidth(); + const hScrollbarWidth = this._rowsView.getScrollbarWidth(true); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferRender(() => { - that._columnHeadersView && that._columnHeadersView.setScrollerSpacing(vScrollbarWidth); - that._footerView && that._footerView.setScrollerSpacing(vScrollbarWidth); - that._rowsView.setScrollerSpacing(vScrollbarWidth, hScrollbarWidth); + this._columnHeadersView?.setScrollerSpacing(vScrollbarWidth); + this._footerView?.setScrollerSpacing(vScrollbarWidth); + this._rowsView.setScrollerSpacing(vScrollbarWidth, hScrollbarWidth); }); } - private _setScrollerSpacing() { + private _setScrollerSpacing(): void { const scrollable = this._rowsView.getScrollable(); // T722415, T758955 const isNativeScrolling = this.option('scrolling.useNative') === true; if (!scrollable || isNativeScrolling) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferRender(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferUpdate(() => { this._setScrollerSpacingCore(); }); @@ -802,7 +850,7 @@ export class ResizingController extends modules.ViewController { /** * @extended: column_fixing */ - protected _setAriaOwns() { + protected _setAriaOwns(): void { const headerTable = this._columnHeadersView?.getTableElement(); const footerTable = this._footerView?.getTableElement(); @@ -813,31 +861,30 @@ export class ResizingController extends modules.ViewController { /** * @extended: header_panel */ - protected _updateDimensionsCore() { - const that = this; - - const dataController = that._dataController; - const rowsView = that._rowsView; + protected _updateDimensionsCore(): void { + const dataController = this._dataController; + const rowsView = this._rowsView; - const $rootElement = that.component.$element(); + const $rootElement = this.component.$element(); const groupElement = this._getGroupElement(); const rootElementHeight = getHeight($rootElement); // @ts-expect-error - const height = that.option('height') ?? $rootElement.get(0).style.height; + const height = this.option('height') ?? $rootElement.get(0).style.height; const isHeightSpecified = !!height && height !== 'auto'; // @ts-expect-error // eslint-disable-next-line radix const maxHeight = parseInt($rootElement.css('maxHeight')); const maxHeightHappened = maxHeight && rootElementHeight >= maxHeight; - // @ts-expect-error - const isMaxHeightApplied = groupElement && groupElement.scrollHeight === groupElement.offsetHeight; + const isMaxHeightApplied = groupElement + && groupElement.scrollHeight === groupElement.offsetHeight; - that.updateSize($rootElement); + this.updateSize($rootElement); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferRender(() => { - const hasHeight = that._hasHeight || !!maxHeight || isHeightSpecified; + const hasHeight = !!this._hasHeight || !!maxHeight || isHeightSpecified; rowsView.hasHeight(hasHeight); this._setAriaOwns(); @@ -851,30 +898,31 @@ export class ResizingController extends modules.ViewController { rowsView.setLoading(dataController.isLoading()); return; } + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget deferUpdate(() => { - that._updateLastSizes($rootElement); - that._setScrollerSpacing(); + this._updateLastSizes($rootElement); + this._setScrollerSpacing(); each(VIEW_NAMES, (index, viewName) => { // TODO getView - const view = that.getView(viewName); + const view = this.getView(viewName); if (view) { view.resize(); } }); - this._editorFactoryController && this._editorFactoryController.resize(); + this._editorFactoryController?.resize(); }); }); } - private _updateLastSizes($rootElement) { + private _updateLastSizes($rootElement: dxElementWrapper): void { this._lastWidth = getWidth($rootElement); this._lastHeight = getHeight($rootElement); this._devicePixelRatio = getWindow().devicePixelRatio; } - public optionChanged(args) { + public optionChanged(args: OptionChanged): void { switch (args.name) { case 'width': case 'height': @@ -896,19 +944,22 @@ export class ResizingController extends modules.ViewController { } export class SynchronizeScrollingController extends modules.ViewController { - private _scrollChangedHandler(views, pos, viewName) { - for (let j = 0; j < views.length; j++) { - if (views[j] && views[j].name !== viewName) { - views[j].scrollTo({ left: pos.left, top: pos.top }); + private _scrollChangedHandler( + views: ColumnsView[], + pos: ScrollPosition, + viewName: string, + ): void { + for (const view of views) { + if (view && view.name !== viewName) { + view.scrollTo({ left: pos.left, top: pos.top }); } } } - public init() { + public init(): void { const views = [this.getView('columnHeadersView'), this.getView('footerView'), this.getView('rowsView')]; - for (let i = 0; i < views.length; i++) { - const view = views[i]; + for (const view of views) { if (view) { view.scrollChanged.add(this._scrollChangedHandler.bind(this, views)); } @@ -921,9 +972,9 @@ export class GridView extends modules.View { private _dataController!: DataController; - private _groupElement: any; + private _groupElement?: dxElementWrapper; - private _rootElement: any; + private _rootElement?: dxElementWrapper; private _a11yGeneralStatusElement!: dxElementWrapper; @@ -932,51 +983,47 @@ export class GridView extends modules.View { this._dataController = this.getController('data'); } - protected _endUpdateCore() { + protected _endUpdateCore(): void { if (this.component._requireResize) { this.component._requireResize = false; this._resizingController.resize(); } } - public getView(name) { + public getView(name: T): Views[T] { return this.component._views[name]; } - public element() { + public element(): dxElementWrapper | undefined { return this._groupElement; } - public optionChanged(args) { - const that = this; - - if (isDefined(that._groupElement) && args.name === 'showBorders') { - that._groupElement.toggleClass(that.addWidgetPrefix(BORDERS_CLASS), !!args.value); + public optionChanged(args: OptionChanged): void { + if (isDefined(this._groupElement) && args.name === 'showBorders') { + this._groupElement.toggleClass(this.addWidgetPrefix(BORDERS_CLASS), !!args.value); args.handled = true; } else { super.optionChanged(args); } } - private _renderViews($groupElement) { - const that = this; - + private _renderViews($groupElement: dxElementWrapper): void { each(VIEW_NAMES, (index, viewName) => { // TODO getView - const view = that.getView(viewName); + const view = this.getView(viewName); if (view) { view.render($groupElement); } }); } - private _getTableRoleName() { + private _getTableRoleName(): string { return 'group'; } - public render($rootElement) { + public render($rootElement: dxElementWrapper): void { const isFirstRender = !this._groupElement; - const $groupElement = this._groupElement || $('
').addClass(this.getWidgetContainerClass()); + const $groupElement = this._groupElement ?? $('
').addClass(this.getWidgetContainerClass()); $groupElement.addClass(GRIDBASE_CONTAINER_CLASS); $groupElement.toggleClass(this.addWidgetPrefix(BORDERS_CLASS), !!this.option('showBorders')); @@ -989,7 +1036,9 @@ export class GridView extends modules.View { if (isFirstRender) { this._groupElement = $groupElement; - hasWindow() && this._resizingController.updateSize($rootElement); + if (hasWindow()) { + this._resizingController.updateSize($rootElement); + } $groupElement.appendTo($rootElement); } @@ -1002,15 +1051,14 @@ export class GridView extends modules.View { this._renderViews($groupElement); } - public update() { - const that = this; - const $rootElement = that._rootElement; - const $groupElement = that._groupElement; + public update(): void { + const $rootElement = this._rootElement; + const $groupElement = this._groupElement; if ($rootElement && $groupElement) { this._resizingController.resize(); - if (that._dataController.isLoaded()) { - that._resizingController.fireContentReadyAction(); + if (this._dataController.isLoaded()) { + this._resizingController.fireContentReadyAction(); } } } @@ -1021,7 +1069,7 @@ export class GridView extends modules.View { } export const gridViewModule = { - defaultOptions() { + defaultOptions(): Pick { return { showBorders: false, renderAsync: false, From 46132bfb88babba6e6adfc38f3624b7ed0492210 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:28:01 +0200 Subject: [PATCH 2/5] Grids: improve readability m_grid_view --- .../__tests__/grid_view.free_width.test.ts | 91 +++++++++++ .../grids/grid_core/views/m_grid_view.ts | 154 +++++++----------- 2 files changed, 152 insertions(+), 93 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts new file mode 100644 index 000000000000..fd6489e8d413 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from '@jest/globals'; + +import type { Column } from '../../columns_controller/types'; +import { ResizingController } from '../m_grid_view'; +import type { RowsView } from '../m_rows_view'; +import type { ColumnWidth } from '../types'; + +const CONTENT_WIDTH = 1000; + +interface ResizingControllerInternals { + _rowsView: Pick; + _getAverageColumnsWidth: (resultWidths: ColumnWidth[]) => number; + _correctColumnWidths: (resultWidths: ColumnWidth[], visibleColumns: Column[]) => boolean; +} + +const createResizingController = (): ResizingControllerInternals => { + const controller = Object.create(ResizingController.prototype) as ResizingControllerInternals; + + controller._rowsView = { contentWidth: (): number => CONTENT_WIDTH }; + + return controller; +}; + +const correctColumnWidths = ( + resultWidths: ColumnWidth[], + visibleColumns: Partial[], +): { isCorrected: boolean; resultWidths: ColumnWidth[] } => { + const isCorrected = createResizingController()._correctColumnWidths( + resultWidths, + visibleColumns as Column[], + ); + + return { isCorrected, resultWidths }; +}; + +describe('ResizingController._getAverageColumnsWidth', () => { + it('splits the free width between the columns without a width', () => { + const averageWidth = createResizingController() + ._getAverageColumnsWidth([200, undefined, 300, undefined]); + + expect(averageWidth).toBe(250); + }); + + it('subtracts percent widths resolved against the content width', () => { + const averageWidth = createResizingController()._getAverageColumnsWidth(['30%', 200, undefined]); + + expect(averageWidth).toBe(500); + }); + + it('returns a negative width when the set widths exceed the content width', () => { + const averageWidth = createResizingController()._getAverageColumnsWidth([700, 500, undefined]); + + expect(averageWidth).toBe(-200); + }); +}); + +describe('ResizingController._correctColumnWidths', () => { + describe('a column without a width and with minWidth', () => { + it('gets its minWidth when the free width is smaller', () => { + const result = correctColumnWidths([undefined, 700], [{ minWidth: 400 }, { width: 700 }]); + + expect(result).toEqual({ isCorrected: true, resultWidths: [400, 700] }); + }); + + it('keeps no width when the free width fits its minWidth', () => { + const result = correctColumnWidths([undefined, 700], [{ minWidth: 200 }, { width: 700 }]); + + expect(result).toEqual({ isCorrected: false, resultWidths: [undefined, 700] }); + }); + }); + + describe('a percent column with minWidth', () => { + it('gets its minWidth when its percent width is smaller', () => { + const result = correctColumnWidths(['30%', undefined], [{ width: '30%', minWidth: 400 }, {}]); + + expect(result).toEqual({ isCorrected: true, resultWidths: [400, undefined] }); + }); + + it('keeps its percent width when it is wider than minWidth and minWidth fits', () => { + const result = correctColumnWidths(['50%', undefined], [{ width: '50%', minWidth: 400 }, {}]); + + expect(result).toEqual({ isCorrected: false, resultWidths: ['50%', undefined] }); + }); + + it('gets its minWidth when minWidth does not fit, even if its percent width is wider', () => { + const result = correctColumnWidths(['150%', undefined], [{ width: '150%', minWidth: 1200 }, {}]); + + expect(result).toEqual({ isCorrected: true, resultWidths: [1200, undefined] }); + }); + }); +}); 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 34605ef6f70b..b24d385cebf0 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 @@ -8,7 +8,6 @@ import type { Callback } from '@js/core/utils/callbacks'; import { deferRender, deferUpdate } from '@js/core/utils/common'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; -import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; import { getHeight, getWidth } from '@js/core/utils/size'; import { setHeight } from '@js/core/utils/style'; @@ -68,37 +67,17 @@ const E2E_ATTRIBUTES = { a11yStatusContainer: 'e2e-a11y-general-status-container', }; -const isPercentWidth = function isPercentWidth(width: ColumnWidth): boolean { - return isString(width) && width.endsWith('%'); -}; - -const isPixelWidth = function isPixelWidth(width: ColumnWidth): boolean { - return isString(width) && width.endsWith('px'); -}; - -const calculateFreeWidth = function calculateFreeWidth( - that: ResizingController, - widths: ColumnWidth[], -): number { - // @ts-expect-error the helper reads non-public members of the controller - const contentWidth = that._rowsView.contentWidth(); - // @ts-expect-error the helper reads non-public members of the controller - const totalWidth = that._getTotalWidth(widths, contentWidth); +const isPercentWidth = (width: ColumnWidth): boolean => isString(width) && width.endsWith('%'); - return contentWidth - totalWidth; -}; +const isPixelWidth = (width: ColumnWidth): boolean => isString(width) && width.endsWith('px'); -const calculateFreeWidthWithCurrentMinWidth = function calculateFreeWidthWithCurrentMinWidth( - that: ResizingController, - columnIndex: number, - currentMinWidth: number, +const withWidthAt = ( widths: ColumnWidth[], -): number { - return calculateFreeWidth( - that, - widths.map((width, index) => (index === columnIndex ? currentMinWidth : width)), - ); -}; + index: number, + width: ColumnWidth, +): ColumnWidth[] => widths.map( + (columnWidth, columnIndex) => (columnIndex === index ? width : columnWidth), +); const restoreFocus = (focusedElement: Element, selectionRange: SelectionRange): void => { accessibility.hiddenFocus(focusedElement, true); @@ -252,27 +231,23 @@ export class ResizingController extends modules.ViewController { } private _setAriaLabel(e?: DataChange): void { - let widgetStatusText = ''; - let labelParts: string[] = []; - - const columnCount = this._columnsController?._columns - ?.filter(({ visible }) => !!visible).length ?? 0; + const visibleColumns = this._columnsController?._columns?.filter(({ visible }) => !!visible); + const columnCount = visibleColumns?.length ?? 0; const totalItemsCount = Math.max(0, this.dataSourceController.totalItemsCount()); const widgetAriaLabel = this._getWidgetAriaLabel(); - widgetStatusText = messageLocalization + const widgetStatusText = messageLocalization // @ts-expect-error Badly typed format method .format(widgetAriaLabel, totalItemsCount, columnCount); // @ts-expect-error Treelist Variable const expandableWidgetAriaLabel = messageLocalization.format(this._expandableWidgetAriaId); - labelParts = [widgetStatusText]; - if (expandableWidgetAriaLabel) { - labelParts.push(expandableWidgetAriaLabel); - } + const labelText = expandableWidgetAriaLabel + ? `${widgetStatusText}. ${expandableWidgetAriaLabel}` + : widgetStatusText; const $ariaLabelElement = this.component.$element().children(`.${GRIDBASE_CONTAINER_CLASS}`); - this.component.setAria('label', labelParts.join('. '), $ariaLabelElement); + this.component.setAria('label', labelText, $ariaLabelElement); if (!e?.isFirstRender) { this._gridView.setWidgetA11yStatusText(widgetStatusText); } @@ -296,7 +271,7 @@ export class ResizingController extends modules.ViewController { private _setVisibleWidths(visibleColumns: Column[], widths: ColumnWidth[]): void { const columnsController = this._columnsController; columnsController.beginUpdate(); - each(visibleColumns, (index, column) => { + visibleColumns.forEach((column, index) => { const columnId = columnsController.getColumnId(column); columnsController.columnOption(columnId, 'visibleWidth', widths[index]); }); @@ -315,18 +290,15 @@ export class ResizingController extends modules.ViewController { const $rowsTables = this._rowsView.getTableElements(); const $viewTables = view.getTableElements(); - each($rowsTables, (index, tableElement) => { - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the if/else - let $tableBody: dxElementWrapper; + $rowsTables.toArray().forEach((tableElement, index) => { const $rowsTable = $(tableElement); const $viewTable = $viewTables.eq(index); if ($viewTable?.length) { - if (isBestFit) { - $tableBody = $viewTable.children('tbody').appendTo($rowsTable); - } else { - $tableBody = $rowsTable.children(`.${className}`).appendTo($viewTable); - } + const $tableBody = isBestFit + ? $viewTable.children('tbody').appendTo($rowsTable) + : $rowsTable.children(`.${className}`).appendTo($viewTable); + $tableBody.toggleClass(className, isBestFit); $tableBody.toggleClass(this.addWidgetPrefix('best-fit'), isBestFit); } @@ -340,13 +312,15 @@ export class ResizingController extends modules.ViewController { const $rowsTable = this._rowsView.getTableElement(); const $rowsFixedTable = this._rowsView.getTableElements().eq(1); - if (!$rowsTable) return; + if (!$rowsTable) { + return; + } $rowsTable.css('tableLayout', isBestFit ? 'auto' : 'fixed'); $rowsTable.children('colgroup').css('display', isBestFit ? 'none' : ''); // NOTE T1156153: Hide group row column to get correct fixed column widths. - each($rowsFixedTable.find(GROUP_ROW_SELECTOR), (idx, item) => { + $rowsFixedTable.find(GROUP_ROW_SELECTOR).toArray().forEach((item) => { $(item).css('display', isBestFit ? 'none' : ''); }); @@ -356,8 +330,7 @@ export class ResizingController extends modules.ViewController { this._toggleBestFitModeForView(this._footerView, CLASSES.footerBody, isBestFit); if (this._needStretch()) { - // @ts-expect-error - $rowsTable.get(0).style.width = isBestFit ? 'auto' : ''; + ($rowsTable.get(0) as HTMLElement).style.width = isBestFit ? 'auto' : ''; } } @@ -419,7 +392,7 @@ export class ResizingController extends modules.ViewController { private synchronizeColumns(): void { const columnsController = this._columnsController; const visibleColumns = columnsController.getVisibleColumns(); - const columnAutoWidth = this.option('columnAutoWidth') as boolean; + const columnAutoWidth = this.option('columnAutoWidth'); 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); @@ -437,7 +410,7 @@ export class ResizingController extends modules.ViewController { resultWidths = this._getBestFitWidths(); } - each(visibleColumns, (index, column) => { + visibleColumns.forEach((column, index) => { if (needBestFit) { const columnId = columnsController.getColumnId(column); columnsController.columnOption(columnId, 'bestFitWidth', resultWidths[index], true); @@ -493,8 +466,15 @@ export class ResizingController extends modules.ViewController { return this._columnsController.getVisibleColumns().some((c) => c.width === 'auto' && !c.command); } + private calculateFreeWidth(widths: ColumnWidth[]): number { + const contentWidth = this._rowsView.contentWidth(); + const totalWidth = this._getTotalWidth(widths, contentWidth); + + return contentWidth - totalWidth; + } + private _getAverageColumnsWidth(resultWidths: ColumnWidth[]): number { - const freeWidth = calculateFreeWidth(this, resultWidths); + const freeWidth = this.calculateFreeWidth(resultWidths); const columnCountWithoutWidth = resultWidths.filter((width) => width === undefined).length; return freeWidth / columnCountWithoutWidth; @@ -506,16 +486,10 @@ export class ResizingController extends modules.ViewController { ): 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]; + const lastExpandColumnIndex = visibleColumns.map(isExpandColumn).lastIndexOf(true); + const expandColumnWidth = lastExpandColumnIndex >= 0 + ? resultWidths[lastExpandColumnIndex] + : undefined; if (!expandColumnWidth) { return; @@ -537,23 +511,16 @@ export class ResizingController extends modules.ViewController { let isColumnWidthsCorrected = false; const hasWidth = this._hasWidth; - for (let i = 0; i < visibleColumns.length; i += 1) { - const index = i; + for (let index = 0; index < visibleColumns.length; index += 1) { const column = visibleColumns[index]; const isHiddenColumn = resultWidths[index] === HIDDEN_COLUMNS_WIDTH; let width = resultWidths[index]; const { minWidth } = column; if (minWidth && width === undefined) { - const averageColumnsWidth = this._getAverageColumnsWidth(resultWidths); - width = averageColumnsWidth; + width = this._getAverageColumnsWidth(resultWidths); } else if (minWidth && isPercentWidth(width)) { - const freeWidth = calculateFreeWidthWithCurrentMinWidth( - this, - index, - minWidth, - resultWidths, - ); + const freeWidth = this.calculateFreeWidth(withWidthAt(resultWidths, index, minWidth)); if (freeWidth < 0) { width = -1; @@ -562,15 +529,13 @@ export class ResizingController extends modules.ViewController { const realColumnWidth = this._getRealColumnWidth( index, - resultWidths.map( - (columnWidth, columnIndex) => (index === columnIndex ? width : columnWidth), - ), + withWidthAt(resultWidths, index, width), ); if (minWidth && !isHiddenColumn && realColumnWidth < minWidth) { resultWidths[index] = minWidth; isColumnWidthsCorrected = true; - i = -1; + index = -1; } if (!isDefined(column.width)) { hasAutoWidth = true; @@ -611,10 +576,12 @@ export class ResizingController extends modules.ViewController { const tableSize = this._getTotalWidth(resultSizes, groupSize); const unusedIndexes = { length: 0 }; - if (!resultSizes.length) return; + if (!resultSizes.length) { + return; + } - each(visibleColumns, function markUnusedIndex(this: Column, index: number) { - if (this.width || resultSizes[index] === HIDDEN_COLUMNS_WIDTH) { + visibleColumns.forEach((column, index) => { + if (column.width || resultSizes[index] === HIDDEN_COLUMNS_WIDTH) { unusedIndexes[index] = true; unusedIndexes.length += 1; } @@ -671,6 +638,7 @@ export class ResizingController extends modules.ViewController { return sum + parseFloat(String(columnWidth)); }, 0); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 0 falls back const currentGroupWidth = groupWidth || this._rowsView.contentWidth(); const freeSpace = currentGroupWidth - pixelTotalWidth; @@ -793,7 +761,7 @@ export class ResizingController extends modules.ViewController { }); }); }); - // @ts-expect-error + // @ts-expect-error deferRender() returns a Deferred when the callback returns nothing }).done(result.resolve).fail(result.reject); }); @@ -844,7 +812,9 @@ export class ResizingController extends modules.ViewController { this._setScrollerSpacingCore(); }); }); - } else { this._setScrollerSpacingCore(); } + } else { + this._setScrollerSpacingCore(); + } } /** @@ -854,7 +824,7 @@ export class ResizingController extends modules.ViewController { const headerTable = this._columnHeadersView?.getTableElement(); const footerTable = this._footerView?.getTableElement(); - // @ts-expect-error + // @ts-expect-error setAriaOwns() is protected and its id params exclude undefined this._rowsView?.setAriaOwns(headerTable?.attr('id'), footerTable?.attr('id')); } @@ -869,13 +839,11 @@ export class ResizingController extends modules.ViewController { const groupElement = this._getGroupElement(); const rootElementHeight = getHeight($rootElement); - // @ts-expect-error - const height = this.option('height') ?? $rootElement.get(0).style.height; + const height = this.option('height') ?? ($rootElement.get(0) as HTMLElement).style.height; const isHeightSpecified = !!height && height !== 'auto'; - // @ts-expect-error - // eslint-disable-next-line radix - const maxHeight = parseInt($rootElement.css('maxHeight')); + // @ts-expect-error css value can be undefined + const maxHeight = parseInt($rootElement.css('maxHeight'), 10); const maxHeightHappened = maxHeight && rootElementHeight >= maxHeight; const isMaxHeightApplied = groupElement && groupElement.scrollHeight === groupElement.offsetHeight; @@ -903,7 +871,7 @@ export class ResizingController extends modules.ViewController { this._updateLastSizes($rootElement); this._setScrollerSpacing(); - each(VIEW_NAMES, (index, viewName) => { + VIEW_NAMES.forEach((viewName) => { // TODO getView const view = this.getView(viewName); if (view) { @@ -1008,7 +976,7 @@ export class GridView extends modules.View { } private _renderViews($groupElement: dxElementWrapper): void { - each(VIEW_NAMES, (index, viewName) => { + VIEW_NAMES.forEach((viewName) => { // TODO getView const view = this.getView(viewName); if (view) { From 69689e2da01634f3415b7fcec76e46ef405da1c5 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:13:59 +0200 Subject: [PATCH 3/5] Grids: cleanup found small issues --- .../extenders/adaptivity_resizing_view_controller.ts | 5 ----- .../grids/grid_core/column_fixing/m_column_fixing.ts | 3 +-- .../js/__internal/grids/grid_core/views/m_grid_view.ts | 3 --- .../js/__internal/grids/grid_core/views/m_rows_view.ts | 6 +++++- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_resizing_view_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_resizing_view_controller.ts index c878f95d56b7..c300b9f7d2f0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_resizing_view_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/extenders/adaptivity_resizing_view_controller.ts @@ -12,11 +12,6 @@ import { COLUMN_VIEWS } from '../const'; export const adaptivityResizingViewControllerExtender = ( Base: ModuleType, ): ModuleType => class AdaptivityResizingViewControllerExtender extends Base { - public dispose() { - super.dispose.apply(this, arguments as any); - clearTimeout(this._updateScrollableTimeoutID); - } - private isHiddenColumnsChanged( oldHiddenColumns: Column[], hiddenColumns: Column[], diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts index 5f077d8ab8ab..2e820a6c1e50 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_fixing/m_column_fixing.ts @@ -915,7 +915,7 @@ const rowsView = (Base: ModuleType) => class RowsViewFixedColumnsExten return deferred; } - protected setAriaOwns(headerTableId, footerTableId, isFixed) { + public setAriaOwns(headerTableId, footerTableId, isFixed) { if (isFixed) { const contentFixedClass = this.addWidgetPrefix(CLASSES.contentFixed); const $contentFixedElement = this.element()?.children(`.${contentFixedClass}`); @@ -1172,7 +1172,6 @@ const resizing = (Base: ModuleType) => class ResizingColumnF const headerFixedTable = this._columnHeadersView?.getFixedTableElement(); // @ts-expect-error const footerFixedTable = this._footerView?.getFixedTableElement(); - // @ts-expect-error this._rowsView?.setAriaOwns(headerFixedTable?.attr('id'), footerFixedTable?.attr('id'), true); } }; 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 b24d385cebf0..d02170c5bb90 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 @@ -119,8 +119,6 @@ export class ResizingController extends modules.ViewController { private _editorFactoryController!: EditorFactory; - protected _updateScrollableTimeoutID?: ReturnType; - public resizeCompleted!: Callback; private isMaxWidthSet = false; @@ -824,7 +822,6 @@ export class ResizingController extends modules.ViewController { const headerTable = this._columnHeadersView?.getTableElement(); const footerTable = this._footerView?.getTableElement(); - // @ts-expect-error setAriaOwns() is protected and its id params exclude undefined this._rowsView?.setAriaOwns(headerTable?.attr('id'), footerTable?.attr('id')); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts index e6859bf4c6a2..7112a58115dc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts @@ -1431,7 +1431,11 @@ export class RowsView extends ColumnsView { /** * @extended: column_fixing */ - protected setAriaOwns(headerTableId: string, footerTableId: string, isFixed?: boolean): void { + public setAriaOwns( + headerTableId: string | undefined, + footerTableId: string | undefined, + isFixed?: boolean, + ): void { const $contentElement = this._findContentElement(); const $tableElement = this.getTableElement(); From 2e96f956cd1a3fa21b9e95fdf8d7eb1b5053a2a9 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:56:08 +0200 Subject: [PATCH 4/5] Grids: drop Object.create from free width tests --- .../grid_view.free_width.integration.test.ts | 122 ++++++++++++++++++ .../__tests__/grid_view.free_width.test.ts | 91 ------------- 2 files changed, 122 insertions(+), 91 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.integration.test.ts delete mode 100644 packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.integration.test.ts new file mode 100644 index 000000000000..a688083f3b45 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.integration.test.ts @@ -0,0 +1,122 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; + +import type { DataGridInstance } from '../../__tests__/__mock__/helpers/utils'; +import { + afterTest, beforeTest, createDataGrid, +} from '../../__tests__/__mock__/helpers/utils'; +import type { ColumnWidth } from '../types'; + +const CONTENT_WIDTH = 1000; + +const createGrid = async ( + columns: DataGridProperties['columns'] = [], +): Promise => { + const { instance } = await createDataGrid({ dataSource: [], columns }); + + $(instance.getView('rowsView').element()).css('width', `${CONTENT_WIDTH}px`); + + return instance; +}; + +const getAverageColumnsWidth = async (resultWidths: ColumnWidth[]): Promise => { + const instance = await createGrid(); + const resizingController = instance.getController('resizing'); + + // @ts-expect-error _getAverageColumnsWidth is private + return resizingController._getAverageColumnsWidth(resultWidths); +}; + +const correctColumnWidths = async ( + columns: DataGridProperties['columns'], + resultWidths: ColumnWidth[], +): Promise<{ isCorrected: boolean; resultWidths: ColumnWidth[] }> => { + const instance = await createGrid(columns); + const resizingController = instance.getController('resizing'); + const visibleColumns = instance.getController('columns').getVisibleColumns(); + + // @ts-expect-error _correctColumnWidths is protected + const isCorrected = resizingController._correctColumnWidths(resultWidths, visibleColumns); + + return { isCorrected, resultWidths }; +}; + +describe('ResizingController._getAverageColumnsWidth', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('splits the free width between the columns without a width', async () => { + const averageWidth = await getAverageColumnsWidth([200, undefined, 300, undefined]); + + expect(averageWidth).toBe(250); + }); + + it('subtracts percent widths resolved against the content width', async () => { + const averageWidth = await getAverageColumnsWidth(['30%', 200, undefined]); + + expect(averageWidth).toBe(500); + }); + + it('returns a negative width when the set widths exceed the content width', async () => { + const averageWidth = await getAverageColumnsWidth([700, 500, undefined]); + + expect(averageWidth).toBe(-200); + }); +}); + +describe('ResizingController._correctColumnWidths', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('a column without a width and with minWidth', () => { + it('gets its minWidth when the free width is smaller', async () => { + const result = await correctColumnWidths( + [{ dataField: 'a', minWidth: 400 }, { dataField: 'b', width: 700 }], + [undefined, 700], + ); + + expect(result).toEqual({ isCorrected: true, resultWidths: [400, 700] }); + }); + + it('keeps no width when the free width fits its minWidth', async () => { + const result = await correctColumnWidths( + [{ dataField: 'a', minWidth: 200 }, { dataField: 'b', width: 700 }], + [undefined, 700], + ); + + expect(result).toEqual({ isCorrected: false, resultWidths: [undefined, 700] }); + }); + }); + + describe('a percent column with minWidth', () => { + it('gets its minWidth when its percent width is smaller', async () => { + const result = await correctColumnWidths( + [{ dataField: 'a', width: '30%', minWidth: 400 }, { dataField: 'b' }], + ['30%', undefined], + ); + + expect(result).toEqual({ isCorrected: true, resultWidths: [400, undefined] }); + }); + + it('keeps its percent width when it is wider than minWidth and minWidth fits', async () => { + const result = await correctColumnWidths( + [{ dataField: 'a', width: '50%', minWidth: 400 }, { dataField: 'b' }], + ['50%', undefined], + ); + + expect(result).toEqual({ isCorrected: false, resultWidths: ['50%', undefined] }); + }); + + it('gets its minWidth when minWidth does not fit, even if its percent width is wider', async () => { + const result = await correctColumnWidths( + [{ dataField: 'a', width: '150%', minWidth: 1200 }, { dataField: 'b' }], + ['150%', undefined], + ); + + expect(result).toEqual({ isCorrected: true, resultWidths: [1200, undefined] }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts b/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts deleted file mode 100644 index fd6489e8d413..000000000000 --- a/packages/devextreme/js/__internal/grids/grid_core/views/__tests__/grid_view.free_width.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import type { Column } from '../../columns_controller/types'; -import { ResizingController } from '../m_grid_view'; -import type { RowsView } from '../m_rows_view'; -import type { ColumnWidth } from '../types'; - -const CONTENT_WIDTH = 1000; - -interface ResizingControllerInternals { - _rowsView: Pick; - _getAverageColumnsWidth: (resultWidths: ColumnWidth[]) => number; - _correctColumnWidths: (resultWidths: ColumnWidth[], visibleColumns: Column[]) => boolean; -} - -const createResizingController = (): ResizingControllerInternals => { - const controller = Object.create(ResizingController.prototype) as ResizingControllerInternals; - - controller._rowsView = { contentWidth: (): number => CONTENT_WIDTH }; - - return controller; -}; - -const correctColumnWidths = ( - resultWidths: ColumnWidth[], - visibleColumns: Partial[], -): { isCorrected: boolean; resultWidths: ColumnWidth[] } => { - const isCorrected = createResizingController()._correctColumnWidths( - resultWidths, - visibleColumns as Column[], - ); - - return { isCorrected, resultWidths }; -}; - -describe('ResizingController._getAverageColumnsWidth', () => { - it('splits the free width between the columns without a width', () => { - const averageWidth = createResizingController() - ._getAverageColumnsWidth([200, undefined, 300, undefined]); - - expect(averageWidth).toBe(250); - }); - - it('subtracts percent widths resolved against the content width', () => { - const averageWidth = createResizingController()._getAverageColumnsWidth(['30%', 200, undefined]); - - expect(averageWidth).toBe(500); - }); - - it('returns a negative width when the set widths exceed the content width', () => { - const averageWidth = createResizingController()._getAverageColumnsWidth([700, 500, undefined]); - - expect(averageWidth).toBe(-200); - }); -}); - -describe('ResizingController._correctColumnWidths', () => { - describe('a column without a width and with minWidth', () => { - it('gets its minWidth when the free width is smaller', () => { - const result = correctColumnWidths([undefined, 700], [{ minWidth: 400 }, { width: 700 }]); - - expect(result).toEqual({ isCorrected: true, resultWidths: [400, 700] }); - }); - - it('keeps no width when the free width fits its minWidth', () => { - const result = correctColumnWidths([undefined, 700], [{ minWidth: 200 }, { width: 700 }]); - - expect(result).toEqual({ isCorrected: false, resultWidths: [undefined, 700] }); - }); - }); - - describe('a percent column with minWidth', () => { - it('gets its minWidth when its percent width is smaller', () => { - const result = correctColumnWidths(['30%', undefined], [{ width: '30%', minWidth: 400 }, {}]); - - expect(result).toEqual({ isCorrected: true, resultWidths: [400, undefined] }); - }); - - it('keeps its percent width when it is wider than minWidth and minWidth fits', () => { - const result = correctColumnWidths(['50%', undefined], [{ width: '50%', minWidth: 400 }, {}]); - - expect(result).toEqual({ isCorrected: false, resultWidths: ['50%', undefined] }); - }); - - it('gets its minWidth when minWidth does not fit, even if its percent width is wider', () => { - const result = correctColumnWidths(['150%', undefined], [{ width: '150%', minWidth: 1200 }, {}]); - - expect(result).toEqual({ isCorrected: true, resultWidths: [1200, undefined] }); - }); - }); -}); From c6c042d347ed2d7e18b2055f721318ef1e2bc454 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:20:06 +0200 Subject: [PATCH 5/5] rebase --- .../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 d02170c5bb90..1b326ba19ce1 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 @@ -31,8 +31,8 @@ import modules from '../modules/modules'; import type { InternalGridOptions, OptionChanged, SelectionRange, Views, } from '../types'; +import type { ColumnsView } from './columns_view'; import { CLASSES } from './const'; -import type { ColumnsView } from './m_columns_view'; import type { RowsView } from './m_rows_view'; import type { ColumnWidth, ScrollPosition, ViewDataChange } from './types';