From 1ef32f74df7bd6bd696573c2e1e2d2602e2e22e8 Mon Sep 17 00:00:00 2001 From: Vaadin Bot Date: Mon, 15 Nov 2021 14:01:28 +0200 Subject: [PATCH] fix: consider all headers and footers when calculating column width (#2964) (#3044) Co-authored-by: Farhad --- packages/grid/src/vaadin-grid.js | 90 ++++++--- packages/grid/test/column-auto-width.test.js | 201 ++++++++++++++++++- 2 files changed, 260 insertions(+), 31 deletions(-) diff --git a/packages/grid/src/vaadin-grid.js b/packages/grid/src/vaadin-grid.js index e8c7d70767..b3d8dcb105 100644 --- a/packages/grid/src/vaadin-grid.js +++ b/packages/grid/src/vaadin-grid.js @@ -535,42 +535,72 @@ class Grid extends ElementMixin( } } + /** @private */ + __getIntrinsicWidth(col) { + const initialWidth = col.width; + const initialFlexGrow = col.flexGrow; + + col.width = 'auto'; + col.flexGrow = 0; + + // Note: _allCells only contains cells which are currently rendered in DOM + const width = col._allCells + .filter((cell) => { + // Exclude body cells that are out of the visible viewport + return !this.$.items.contains(cell) || this._isInViewport(cell.parentElement); + }) + .reduce((width, cell) => { + // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering) + return Math.max(width, cell.offsetWidth + 1); + }, 0); + + col.flexGrow = initialFlexGrow; + col.width = initialWidth; + + return width; + } + + /** @private */ + __getDistributedWidth(col, innerColumn) { + if (col == null || col === this) return 0; + + const columnWidth = Math.max(this.__getIntrinsicWidth(col), this.__getDistributedWidth(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) { - // Note: The `cols.forEach()` loops below could be implemented as a single loop but this has been - // split for performance reasons to batch these similar actions [write/read] together to avoid - // unnecessary layout trashing. - - // [write] Set automatic width for all cells (breaks column alignment) - cols.forEach((col) => { - col.width = 'auto'; - col._origFlexGrow = col.flexGrow; - col.flexGrow = 0; - }); - // [read] Measure max cell width in each column - cols.forEach((col) => { - col._currentWidth = 0; - // Note: _allCells only contains cells which are currently rendered in DOM - col._allCells - .filter((c) => { - // Exclude body cells that are out of the visible viewport - return !this.$.items.contains(c) || this._isInViewport(c.parentElement); - }) - .forEach((c) => { - // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering) - const cellWidth = c.offsetWidth + 1; - col._currentWidth = Math.max(col._currentWidth, cellWidth); - }); - }); - // [write] Set column widths to fit widest measured content cols.forEach((col) => { - col.width = `${col._currentWidth}px`; - col.flexGrow = col._origFlexGrow; - col._currentWidth = undefined; - col._origFlexGrow = undefined; + col.width = `${this.__getDistributedWidth(col)}px`; }); } diff --git a/packages/grid/test/column-auto-width.test.js b/packages/grid/test/column-auto-width.test.js index 9bbd5b13c7..68947e6278 100644 --- a/packages/grid/test/column-auto-width.test.js +++ b/packages/grid/test/column-auto-width.test.js @@ -2,6 +2,7 @@ import { expect } from '@esm-bundle/chai'; import { fixtureSync, nextFrame, oneEvent } from '@vaadin/testing-helpers'; import sinon from 'sinon'; import '../vaadin-grid.js'; +import '../vaadin-grid-column-group.js'; import '../vaadin-grid-tree-column.js'; import { flushGrid } from './helpers.js'; @@ -116,7 +117,7 @@ describe('column auto-width', function () { it('should exclude non-visible body cells from grid column auto width calc', async () => { // Assign more items to the grid. The last one with the long content, while in the DOM, - // will end up outside the visible viewport and therefor should not affect the + // will end up outside the visible viewport and therefore should not affect the // calculated column auto-width grid.items = [...testItems, { a: 'a' }, { a: 'aaaaaaaaaaaaaaaaaaaaa' }]; @@ -161,3 +162,201 @@ describe('async recalculateWidth columns', () => { expect(grid._recalculateColumnWidths.called).to.be.true; }); }); + +describe('column group', () => { + const num = (str) => parseInt(str, 10); + + function expectColumnsWidthToBeOk(grid, ws, delta = 5) { + const columns = grid.querySelectorAll('vaadin-grid-column'); + + Array.from(columns).forEach((col, i) => { + const columnWidth = num(col.width); + expect(columnWidth).to.be.closeTo(ws[i], delta); + }); + } + + function createGrid(html, items = [{ a: 'm', b: 'mm' }]) { + const grid = fixtureSync(html); + grid.items = items; + flushGrid(grid); + + return grid; + } + + it('should consider column group when calculating column width', () => { + const grid = createGrid(` + + + + + + `); + expectColumnsWidthToBeOk(grid, [420], 25); + }); + + it('should distribute the excess space to all columns', () => { + const grid = createGrid(` + + + + + + + `); + + expectColumnsWidthToBeOk(grid, [217, 217], 20); + }); + + it('should distribute the extra necessary space to all columns regardless of flexGrow', () => { + const items = [{ first: 'fff', last: 'lll' }]; + + const grid = createGrid( + ` + + + + + + + `, + items + ); + + const gridWithFlexGrow = createGrid( + ` + + + + + + + `, + items + ); + + const [columnA, columnB] = grid.querySelectorAll('vaadin-grid-column'); + const [columnA2, columnB2] = gridWithFlexGrow.querySelectorAll('vaadin-grid-column'); + + expect(columnA.width).to.equal(columnA2.width); + expect(columnB.width).to.equal(columnB2.width); + }); + + it('should distribute the excess space to all columns according to their initial width', () => { + const grid = createGrid(` + + + + + + + `); + + const [columnA, columnB] = grid.querySelectorAll('vaadin-grid-column'); + expect(num(columnB.width)).to.be.greaterThan(num(columnA.width)); + }); + + it('should consider all the parent vaadin-grid-column-groups when calculating the necessary width', () => { + const grid = createGrid(` + + + + + + + + `); + expectColumnsWidthToBeOk(grid, [403], 30); + }); + + it('should consider vaadin-grid-column header when calculating column width', () => { + const grid = createGrid(` + + + + + + `); + expectColumnsWidthToBeOk(grid, [420], 25); + }); + + it('should consider vaadin-grid-column-group footer when calculating column width', () => { + const grid = createGrid(` + + + + + + `); + + const columnGroup = document.querySelector('vaadin-grid-column-group'); + const column = document.querySelector('vaadin-grid-column'); + + columnGroup.footerRenderer = (root) => { + const footer = document.createElement('footer'); + footer.textContent = 'group footer'; + footer.style.width = '300px'; + + root.appendChild(footer); + }; + + column.footerRenderer = (root) => { + const footer = document.createElement('footer'); + footer.textContent = 'column footer'; + root.appendChild(footer); + }; + + grid.recalculateColumnWidths(); + expectColumnsWidthToBeOk(grid, [333]); + }); + + it('should consider vaadin-grid-column footer when calculating column width', () => { + const grid = createGrid(` + + + + + + `); + + const columnGroup = document.querySelector('vaadin-grid-column-group'); + const column = document.querySelector('vaadin-grid-column'); + + columnGroup.footerRenderer = (root) => { + const footer = document.createElement('footer'); + footer.textContent = 'group footer'; + root.appendChild(footer); + }; + + column.footerRenderer = (root) => { + const footer = document.createElement('footer'); + footer.textContent = 'footer'; + footer.style.width = '300px'; + + root.appendChild(footer); + }; + + grid.recalculateColumnWidths(); + expectColumnsWidthToBeOk(grid, [333]); + }); + + it('should not error when there is no vaadin-grid-column-group', () => { + const grid = createGrid(` + + + + `); + + const column = document.querySelector('vaadin-grid-column'); + + column.footerRenderer = (root) => { + const footer = document.createElement('footer'); + footer.textContent = 'footer'; + footer.style.width = '300px'; + + root.appendChild(footer); + }; + + grid.recalculateColumnWidths(); + expectColumnsWidthToBeOk(grid, [333]); + }); +});