Skip to content

Commit

Permalink
fix: consider all headers and footers when calculating column width (#…
Browse files Browse the repository at this point in the history
…2964) (#3044)

Co-authored-by: Farhad <farhad@vaadin.com>
  • Loading branch information
vaadin-bot and Farhad committed Nov 15, 2021
1 parent 3544a2c commit 1ef32f7
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 31 deletions.
90 changes: 60 additions & 30 deletions packages/grid/src/vaadin-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<!GridColumn>} 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`;
});
}

Expand Down
201 changes: 200 additions & 1 deletion packages/grid/test/column-auto-width.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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' }];

Expand Down Expand Up @@ -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(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="a lengthy header that should change the width of the column">
<vaadin-grid-column auto-width path="a" header="small header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);
expectColumnsWidthToBeOk(grid, [420], 25);
});

it('should distribute the excess space to all columns', () => {
const grid = createGrid(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="a lengthy header that should change the width of the column">
<vaadin-grid-column auto-width path="a" header="small header"></vaadin-grid-column>
<vaadin-grid-column auto-width path="b" header="small header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);

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(
`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="HeaderHeaderHeaderHeaderHeaderHeaderHeaderHeaderHeaderHeader">
<vaadin-grid-column auto-width path="first"></vaadin-grid-column>
<vaadin-grid-column auto-width path="last"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`,
items
);

const gridWithFlexGrow = createGrid(
`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="HeaderHeaderHeaderHeaderHeaderHeaderHeaderHeaderHeaderHeader">
<vaadin-grid-column auto-width flex-grow="3" path="first"></vaadin-grid-column>
<vaadin-grid-column auto-width path="last"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`,
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(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="a lengthy header that should change the width of the column">
<vaadin-grid-column auto-width path="a" header="header"></vaadin-grid-column>
<vaadin-grid-column auto-width path="b" header="headerheader"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);

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(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="a lengthy header, greater than immediate column-group">
<vaadin-grid-column-group header="immediate column-group">
<vaadin-grid-column auto-width path="a" header="header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid-column-group>
</vaadin-grid>
`);
expectColumnsWidthToBeOk(grid, [403], 30);
});

it('should consider vaadin-grid-column header when calculating column width', () => {
const grid = createGrid(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group header="small header">
<vaadin-grid-column auto-width path="a" header="a lengthy header that should change the width of the column"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);
expectColumnsWidthToBeOk(grid, [420], 25);
});

it('should consider vaadin-grid-column-group footer when calculating column width', () => {
const grid = createGrid(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group>
<vaadin-grid-column auto-width path="a" header="header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);

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(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column-group>
<vaadin-grid-column auto-width path="a" header="header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid>
`);

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(`
<vaadin-grid style="width: 200px">
<vaadin-grid-column auto-width path="a" header="header"></vaadin-grid-column>
</vaadin-grid>
`);

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]);
});
});

0 comments on commit 1ef32f7

Please sign in to comment.