diff --git a/packages/grid/src/vaadin-grid-column-auto-width-mixin.d.ts b/packages/grid/src/vaadin-grid-column-auto-width-mixin.d.ts new file mode 100644 index 0000000000..64716fff02 --- /dev/null +++ b/packages/grid/src/vaadin-grid-column-auto-width-mixin.d.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright (c) 2016 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import type { Constructor } from '@open-wc/dedupe-mixin'; + +export declare function ColumnAutoWidthMixin>( + base: T, +): Constructor & T; + +export declare class ColumnAutoWidthMixinClass { + /** + * Updates the `width` of all columns which have `autoWidth` set to `true`. + */ + recalculateColumnWidths(): void; +} diff --git a/packages/grid/src/vaadin-grid-column-auto-width-mixin.js b/packages/grid/src/vaadin-grid-column-auto-width-mixin.js new file mode 100644 index 0000000000..cdfd6b4e35 --- /dev/null +++ b/packages/grid/src/vaadin-grid-column-auto-width-mixin.js @@ -0,0 +1,226 @@ +/** + * @license + * Copyright (c) 2016 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js'; + +/** + * A mixin providing grid column auto-width functionality. + * + * @polymerMixin + */ +export const ColumnAutoWidthMixin = (superClass) => + class extends superClass { + static get properties() { + return { + /** @private */ + __pendingRecalculateColumnWidths: { + type: Boolean, + value: true, + }, + }; + } + + /** @private */ + __getIntrinsicWidth(col) { + if (!this.__intrinsicWidthCache.has(col)) { + this.__calculateAndCacheIntrinsicWidths([col]); + } + return this.__intrinsicWidthCache.get(col); + } + + /** @private */ + __getDistributedWidth(col, innerColumn) { + if (col == null || col === this) { + return 0; + } + + const columnWidth = Math.max( + this.__getIntrinsicWidth(col), + this.__getDistributedWidth((col.assignedSlot || col).parentElement, col), + ); + + // We're processing a regular grid-column and not a grid-column-group + if (!innerColumn) { + return columnWidth; + } + + // At the end, the width of each vaadin-grid-column-group is determined by the sum of the width of its children. + // Here we determine how much space the vaadin-grid-column-group actually needs to render properly and then we distribute that space + // to its children, so when we actually do the summation it will be rendered properly. + // Check out vaadin-grid-column-group:_updateFlexAndWidth + const columnGroup = col; + const columnGroupWidth = columnWidth; + const sumOfWidthOfAllChildColumns = columnGroup._visibleChildColumns + .map((col) => this.__getIntrinsicWidth(col)) + .reduce((sum, curr) => sum + curr, 0); + + const extraNecessarySpaceForGridColumnGroup = Math.max(0, columnGroupWidth - sumOfWidthOfAllChildColumns); + + // The distribution of the extra necessary space is done according to the intrinsic width of each child column. + // Lets say we need 100 pixels of extra space for the grid-column-group to render properly + // it has two grid-column children, |100px|300px| in total 400px + // the first column gets 25px of the additional space (100/400)*100 = 25 + // the second column gets the 75px of the additional space (300/400)*100 = 75 + const proportionOfExtraSpace = this.__getIntrinsicWidth(innerColumn) / sumOfWidthOfAllChildColumns; + const shareOfInnerColumnFromNecessaryExtraSpace = proportionOfExtraSpace * extraNecessarySpaceForGridColumnGroup; + + return this.__getIntrinsicWidth(innerColumn) + shareOfInnerColumnFromNecessaryExtraSpace; + } + + /** + * @param {!Array} cols the columns to auto size based on their content width + * @private + */ + _recalculateColumnWidths(cols) { + // Flush to make sure DOM is up-to-date when measuring the column widths + this.__virtualizer.flush(); + [...this.$.header.children, ...this.$.footer.children].forEach((row) => { + if (row.__debounceUpdateHeaderFooterRowVisibility) { + row.__debounceUpdateHeaderFooterRowVisibility.flush(); + } + }); + + // Flush to account for any changes to the visibility of the columns + if (this._debouncerHiddenChanged) { + this._debouncerHiddenChanged.flush(); + } + + this.__intrinsicWidthCache = new Map(); + // Cache the viewport rows to avoid unnecessary reflows while measuring the column widths + const fvi = this._firstVisibleIndex; + const lvi = this._lastVisibleIndex; + this.__viewportRowsCache = this._getRenderedRows().filter((row) => row.index >= fvi && row.index <= lvi); + + // Pre-cache the intrinsic width of each column + this.__calculateAndCacheIntrinsicWidths(cols); + + cols.forEach((col) => { + col.width = `${this.__getDistributedWidth(col)}px`; + }); + } + + /** + * Toggles the cell content for the given column to use or not use auto width. + * + * While content for all the column cells uses auto width (instead of the default 100%), + * their offsetWidth can be used to calculate the collective intrinsic width of the column. + * + * @private + */ + __setVisibleCellContentAutoWidth(col, autoWidth) { + col._allCells + .filter((cell) => { + if (this.$.items.contains(cell)) { + return this.__viewportRowsCache.includes(cell.parentElement); + } + return true; + }) + .forEach((cell) => { + cell.__measuringAutoWidth = autoWidth; + + if (cell.__measuringAutoWidth) { + // Store the original inline width of the cell to restore it later + cell.__originalWidth = cell.style.width; + // Prepare the cell for having its intrinsic width measured + cell.style.width = 'auto'; + cell.style.position = 'absolute'; + } else { + // Restore the original width + cell.style.width = cell.__originalWidth; + delete cell.__originalWidth; + cell.style.position = ''; + } + }); + + if (autoWidth) { + this.$.scroller.setAttribute('measuring-auto-width', ''); + } else { + this.$.scroller.removeAttribute('measuring-auto-width'); + } + } + + /** + * Returns the maximum intrinsic width of the cell content in the given column. + * Only cells which are marked for measuring auto width are considered. + * + * @private + */ + __getAutoWidthCellsMaxWidth(col) { + // Note: _allCells only contains cells which are currently rendered in DOM + return col._allCells.reduce((width, cell) => { + // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering) + return cell.__measuringAutoWidth ? Math.max(width, cell.offsetWidth + 1) : width; + }, 0); + } + + /** + * Calculates and caches the intrinsic width of each given column. + * + * @private + */ + __calculateAndCacheIntrinsicWidths(cols) { + // Make all the columns use auto width at once before measuring to + // avoid reflows in between the measurements + cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, true)); + // Measure and cache + cols.forEach((col) => { + const width = this.__getAutoWidthCellsMaxWidth(col); + this.__intrinsicWidthCache.set(col, width); + }); + // Reset the columns to use 100% width + cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, false)); + } + + /** + * Updates the `width` of all columns which have `autoWidth` set to `true`. + */ + recalculateColumnWidths() { + if (!this._columnTree) { + return; // No columns + } + if (isElementHidden(this) || this._dataProviderController.isLoading()) { + this.__pendingRecalculateColumnWidths = true; + return; + } + const cols = this._getColumns().filter((col) => !col.hidden && col.autoWidth); + + const undefinedCols = cols.filter((col) => !customElements.get(col.localName)); + if (undefinedCols.length) { + // Some of the columns are not defined yet, wait for them to be defined before measuring + Promise.all(undefinedCols.map((col) => customElements.whenDefined(col.localName))).then(() => { + this._recalculateColumnWidths(cols); + }); + } else { + this._recalculateColumnWidths(cols); + } + } + + /** @private */ + __tryToRecalculateColumnWidthsIfPending() { + if (!this.__pendingRecalculateColumnWidths || isElementHidden(this) || this._dataProviderController.isLoading()) { + return; + } + + // Delay recalculation if any rows are missing an index. + // This can happen during the grid's initialization if the recalculation is triggered + // as a result of the data provider responding synchronously to a page request created + // in the middle of the virtualizer update loop. In this case, rows after the one that + // triggered the page request may not have an index property yet. The lack of index + // prevents _onDataProviderPageReceived from requesting children for these rows, + // resulting in loading state being set to false and the recalculation beginning + // before all the data is loaded. Note, rows without index get updated in later iterations + // of the virtualizer update loop, ensuring the grid eventually reaches a stable state. + const hasRowsWithUndefinedIndex = [...this.$.items.children].some((row) => row.index === undefined); + if (hasRowsWithUndefinedIndex) { + return; + } + + const hasRowsWithClientHeight = [...this.$.items.children].some((row) => row.clientHeight > 0); + if (hasRowsWithClientHeight) { + this.__pendingRecalculateColumnWidths = false; + this.recalculateColumnWidths(); + } + } + }; diff --git a/packages/grid/src/vaadin-grid-mixin.d.ts b/packages/grid/src/vaadin-grid-mixin.d.ts index e9d818a82f..40e0004aae 100644 --- a/packages/grid/src/vaadin-grid-mixin.d.ts +++ b/packages/grid/src/vaadin-grid-mixin.d.ts @@ -9,6 +9,7 @@ import type { A11yMixinClass } from './vaadin-grid-a11y-mixin.js'; import type { ActiveItemMixinClass } from './vaadin-grid-active-item-mixin.js'; import type { ArrayDataProviderMixinClass } from './vaadin-grid-array-data-provider-mixin.js'; import type { GridBodyRenderer, GridColumn, GridHeaderFooterRenderer } from './vaadin-grid-column.js'; +import type { ColumnAutoWidthMixinClass } from './vaadin-grid-column-auto-width-mixin.js'; import type { ColumnReorderingMixinClass } from './vaadin-grid-column-reordering-mixin.js'; import type { DataProviderMixinClass, @@ -172,6 +173,7 @@ export declare function GridMixin>( base: T, ): Constructor & Constructor> & + Constructor & Constructor> & Constructor & Constructor> & @@ -191,6 +193,7 @@ export interface GridMixinClass A11yMixinClass, ActiveItemMixinClass, ArrayDataProviderMixinClass, + ColumnAutoWidthMixinClass, DataProviderMixinClass, RowDetailsMixinClass, ScrollMixinClass, @@ -209,11 +212,6 @@ export interface GridMixinClass */ allRowsVisible: boolean; - /** - * Updates the `width` of all columns which have `autoWidth` set to `true`. - */ - recalculateColumnWidths(): void; - /** * Requests an update for the content of cells. * diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js index 207bf42182..5598cbed37 100644 --- a/packages/grid/src/vaadin-grid-mixin.js +++ b/packages/grid/src/vaadin-grid-mixin.js @@ -3,7 +3,6 @@ * Copyright (c) 2016 - 2025 Vaadin Ltd. * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ */ -import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js'; import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js'; import { animationFrame, microTask } from '@vaadin/component-base/src/async.js'; import { @@ -24,6 +23,7 @@ import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js'; import { A11yMixin } from './vaadin-grid-a11y-mixin.js'; import { ActiveItemMixin } from './vaadin-grid-active-item-mixin.js'; import { ArrayDataProviderMixin } from './vaadin-grid-array-data-provider-mixin.js'; +import { ColumnAutoWidthMixin } from './vaadin-grid-column-auto-width-mixin.js'; import { ColumnReorderingMixin } from './vaadin-grid-column-reordering-mixin.js'; import { ColumnResizingMixin } from './vaadin-grid-column-resizing-mixin.js'; import { DataProviderMixin } from './vaadin-grid-data-provider-mixin.js'; @@ -80,7 +80,9 @@ export const GridMixin = (superClass) => FilterMixin( ColumnReorderingMixin( ColumnResizingMixin( - EventContextMixin(DragAndDropMixin(StylingMixin(TabindexMixin(superClass)))), + EventContextMixin( + DragAndDropMixin(ColumnAutoWidthMixin(StylingMixin(TabindexMixin(superClass)))), + ), ), ), ), @@ -144,12 +146,6 @@ export const GridMixin = (superClass) => reflectToAttribute: true, }, - /** @private */ - __pendingRecalculateColumnWidths: { - type: Boolean, - value: true, - }, - /** @private */ isAttached: { value: false, @@ -352,208 +348,6 @@ export const GridMixin = (superClass) => } } - /** @private */ - __getIntrinsicWidth(col) { - if (!this.__intrinsicWidthCache.has(col)) { - this.__calculateAndCacheIntrinsicWidths([col]); - } - return this.__intrinsicWidthCache.get(col); - } - - /** @private */ - __getDistributedWidth(col, innerColumn) { - if (col == null || col === this) { - return 0; - } - - const columnWidth = Math.max( - this.__getIntrinsicWidth(col), - this.__getDistributedWidth((col.assignedSlot || col).parentElement, col), - ); - - // We're processing a regular grid-column and not a grid-column-group - if (!innerColumn) { - return columnWidth; - } - - // At the end, the width of each vaadin-grid-column-group is determined by the sum of the width of its children. - // Here we determine how much space the vaadin-grid-column-group actually needs to render properly and then we distribute that space - // to its children, so when we actually do the summation it will be rendered properly. - // Check out vaadin-grid-column-group:_updateFlexAndWidth - const columnGroup = col; - const columnGroupWidth = columnWidth; - const sumOfWidthOfAllChildColumns = columnGroup._visibleChildColumns - .map((col) => this.__getIntrinsicWidth(col)) - .reduce((sum, curr) => sum + curr, 0); - - const extraNecessarySpaceForGridColumnGroup = Math.max(0, columnGroupWidth - sumOfWidthOfAllChildColumns); - - // The distribution of the extra necessary space is done according to the intrinsic width of each child column. - // Lets say we need 100 pixels of extra space for the grid-column-group to render properly - // it has two grid-column children, |100px|300px| in total 400px - // the first column gets 25px of the additional space (100/400)*100 = 25 - // the second column gets the 75px of the additional space (300/400)*100 = 75 - const proportionOfExtraSpace = this.__getIntrinsicWidth(innerColumn) / sumOfWidthOfAllChildColumns; - const shareOfInnerColumnFromNecessaryExtraSpace = proportionOfExtraSpace * extraNecessarySpaceForGridColumnGroup; - - return this.__getIntrinsicWidth(innerColumn) + shareOfInnerColumnFromNecessaryExtraSpace; - } - - /** - * @param {!Array} cols the columns to auto size based on their content width - * @private - */ - _recalculateColumnWidths(cols) { - // Flush to make sure DOM is up-to-date when measuring the column widths - this.__virtualizer.flush(); - [...this.$.header.children, ...this.$.footer.children].forEach((row) => { - if (row.__debounceUpdateHeaderFooterRowVisibility) { - row.__debounceUpdateHeaderFooterRowVisibility.flush(); - } - }); - - // Flush to account for any changes to the visibility of the columns - if (this._debouncerHiddenChanged) { - this._debouncerHiddenChanged.flush(); - } - - this.__intrinsicWidthCache = new Map(); - // Cache the viewport rows to avoid unnecessary reflows while measuring the column widths - const fvi = this._firstVisibleIndex; - const lvi = this._lastVisibleIndex; - this.__viewportRowsCache = this._getRenderedRows().filter((row) => row.index >= fvi && row.index <= lvi); - - // Pre-cache the intrinsic width of each column - this.__calculateAndCacheIntrinsicWidths(cols); - - cols.forEach((col) => { - col.width = `${this.__getDistributedWidth(col)}px`; - }); - } - - /** - * Toggles the cell content for the given column to use or not use auto width. - * - * While content for all the column cells uses auto width (instead of the default 100%), - * their offsetWidth can be used to calculate the collective intrinsic width of the column. - * - * @private - */ - __setVisibleCellContentAutoWidth(col, autoWidth) { - col._allCells - .filter((cell) => { - if (this.$.items.contains(cell)) { - return this.__viewportRowsCache.includes(cell.parentElement); - } - return true; - }) - .forEach((cell) => { - cell.__measuringAutoWidth = autoWidth; - - if (cell.__measuringAutoWidth) { - // Store the original inline width of the cell to restore it later - cell.__originalWidth = cell.style.width; - // Prepare the cell for having its intrinsic width measured - cell.style.width = 'auto'; - cell.style.position = 'absolute'; - } else { - // Restore the original width - cell.style.width = cell.__originalWidth; - delete cell.__originalWidth; - cell.style.position = ''; - } - }); - - if (autoWidth) { - this.$.scroller.setAttribute('measuring-auto-width', ''); - } else { - this.$.scroller.removeAttribute('measuring-auto-width'); - } - } - - /** - * Returns the maximum intrinsic width of the cell content in the given column. - * Only cells which are marked for measuring auto width are considered. - * - * @private - */ - __getAutoWidthCellsMaxWidth(col) { - // Note: _allCells only contains cells which are currently rendered in DOM - return col._allCells.reduce((width, cell) => { - // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering) - return cell.__measuringAutoWidth ? Math.max(width, cell.offsetWidth + 1) : width; - }, 0); - } - - /** - * Calculates and caches the intrinsic width of each given column. - * - * @private - */ - __calculateAndCacheIntrinsicWidths(cols) { - // Make all the columns use auto width at once before measuring to - // avoid reflows in between the measurements - cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, true)); - // Measure and cache - cols.forEach((col) => { - const width = this.__getAutoWidthCellsMaxWidth(col); - this.__intrinsicWidthCache.set(col, width); - }); - // Reset the columns to use 100% width - cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, false)); - } - - /** - * Updates the `width` of all columns which have `autoWidth` set to `true`. - */ - recalculateColumnWidths() { - if (!this._columnTree) { - return; // No columns - } - if (isElementHidden(this) || this._dataProviderController.isLoading()) { - this.__pendingRecalculateColumnWidths = true; - return; - } - const cols = this._getColumns().filter((col) => !col.hidden && col.autoWidth); - - const undefinedCols = cols.filter((col) => !customElements.get(col.localName)); - if (undefinedCols.length) { - // Some of the columns are not defined yet, wait for them to be defined before measuring - Promise.all(undefinedCols.map((col) => customElements.whenDefined(col.localName))).then(() => { - this._recalculateColumnWidths(cols); - }); - } else { - this._recalculateColumnWidths(cols); - } - } - - /** @private */ - __tryToRecalculateColumnWidthsIfPending() { - if (!this.__pendingRecalculateColumnWidths || isElementHidden(this) || this._dataProviderController.isLoading()) { - return; - } - - // Delay recalculation if any rows are missing an index. - // This can happen during the grid's initialization if the recalculation is triggered - // as a result of the data provider responding synchronously to a page request created - // in the middle of the virtualizer update loop. In this case, rows after the one that - // triggered the page request may not have an index property yet. The lack of index - // prevents _onDataProviderPageReceived from requesting children for these rows, - // resulting in loading state being set to false and the recalculation beginning - // before all the data is loaded. Note, rows without index get updated in later iterations - // of the virtualizer update loop, ensuring the grid eventually reaches a stable state. - const hasRowsWithUndefinedIndex = [...this.$.items.children].some((row) => row.index === undefined); - if (hasRowsWithUndefinedIndex) { - return; - } - - const hasRowsWithClientHeight = [...this.$.items.children].some((row) => row.clientHeight > 0); - if (hasRowsWithClientHeight) { - this.__pendingRecalculateColumnWidths = false; - this.recalculateColumnWidths(); - } - } - /** * @protected * @override