diff --git a/packages/grid/src/vaadin-grid-column-mixin.d.ts b/packages/grid/src/vaadin-grid-column-mixin.d.ts index 3b6a70ef1dd..3ad77b78b92 100644 --- a/packages/grid/src/vaadin-grid-column-mixin.d.ts +++ b/packages/grid/src/vaadin-grid-column-mixin.d.ts @@ -126,6 +126,15 @@ export declare class GridColumnMixinClass< */ width: string | null | undefined; + /** + * Min-width of the cells for this column. + * + * Please note that using the `em` length unit is discouraged as + * it might lead to misalignment issues if the header, body, and footer + * cells have different font sizes. Instead, use `rem` if you need + * a length unit relative to the font size. + */ + minWidth: string | null | undefined; /** * Flex grow ratio for the cell widths. When set to 0, cell width is fixed. * @attr {number} flex-grow diff --git a/packages/grid/src/vaadin-grid-column-mixin.js b/packages/grid/src/vaadin-grid-column-mixin.js index 62200a1393a..a6366bd7cf2 100644 --- a/packages/grid/src/vaadin-grid-column-mixin.js +++ b/packages/grid/src/vaadin-grid-column-mixin.js @@ -268,6 +268,7 @@ export const ColumnBaseMixin = (superClass) => static get observers() { return [ '_widthChanged(width, _headerCell, _footerCell, _cells)', + '_minWidthChanged(minWidth, _headerCell, _footerCell, _cells)', '_frozenChanged(frozen, _headerCell, _footerCell, _cells)', '_frozenToEndChanged(frozenToEnd, _headerCell, _footerCell, _cells)', '_flexGrowChanged(flexGrow, _headerCell, _footerCell, _cells)', @@ -406,6 +407,17 @@ export const ColumnBaseMixin = (superClass) => }); } + /** @private */ + _minWidthChanged(minWidth) { + if (this.parentElement && this.parentElement._columnPropChanged) { + this.parentElement._columnPropChanged('minWidth'); + } + + this._allCells.forEach((cell) => { + cell.style.minWidth = minWidth; + }); + } + /** @private */ _frozenChanged(frozen) { if (this.parentElement && this.parentElement._columnPropChanged) { @@ -863,6 +875,18 @@ export const GridColumnMixin = (superClass) => value: '100px', sync: true, }, + /** + * Min-width of the cells for this column. + * + * Please note that using the `em` length unit is discouraged as + * it might lead to misalignment issues if the header, body, and footer + * cells have different font sizes. Instead, use `rem` if you need + * a length unit relative to the font size. + */ + minWidth: { + type: String, + sync: true, + }, /** * Flex grow ratio for the cell widths. When set to 0, cell width is fixed. diff --git a/packages/grid/src/vaadin-grid-column-resizing-mixin.js b/packages/grid/src/vaadin-grid-column-resizing-mixin.js index a2c8f7e6e5b..fe1cdeeb33b 100644 --- a/packages/grid/src/vaadin-grid-column-resizing-mixin.js +++ b/packages/grid/src/vaadin-grid-column-resizing-mixin.js @@ -77,8 +77,12 @@ export const ColumnResizingMixin = (superClass) => } else { maxWidth = cellWidth + (isRTL ? cellRect.left - eventX : eventX - cellRect.right); } - - column.width = `${Math.max(minWidth, maxWidth)}px`; + const calculatedWidth = Math.max(minWidth, maxWidth); + if (column.minWidth) { + column.width = `max(${column.minWidth}, ${calculatedWidth}px)`; + } else { + column.width = `${calculatedWidth}px`; + } column.flexGrow = 0; } // Fix width and flex-grow for all preceding columns diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js index 1535a1fe397..380e4210e14 100644 --- a/packages/grid/src/vaadin-grid-mixin.js +++ b/packages/grid/src/vaadin-grid-mixin.js @@ -409,7 +409,12 @@ export const GridMixin = (superClass) => this.__calculateAndCacheIntrinsicWidths(cols); cols.forEach((col) => { - col.width = `${this.__getDistributedWidth(col)}px`; + const calculatedWidth = this.__getDistributedWidth(col); + if (col.minWidth) { + col.width = `max(${col.minWidth}, ${calculatedWidth}px)`; + } else { + col.width = `${calculatedWidth}px`; + } }); } @@ -507,6 +512,17 @@ export const GridMixin = (superClass) => } else { this._recalculateColumnWidths(cols); } + // update the columns without autowidth and set the min width + const noAutoWidthCols = this._getColumns().filter((col) => !col.hidden && !col.autoWidth); + + noAutoWidthCols.forEach((col) => { + const calculatedWidth = col.width; + if (col.minWidth) { + col.width = `max(${col.minWidth}, ${calculatedWidth})`; + } else { + col.width = `${calculatedWidth}`; + } + }); } /** @private */ diff --git a/packages/grid/test/column-min-width-lit.test.ts b/packages/grid/test/column-min-width-lit.test.ts new file mode 100644 index 00000000000..eaf800c03da --- /dev/null +++ b/packages/grid/test/column-min-width-lit.test.ts @@ -0,0 +1,3 @@ +import '../theme/lumo/lit-all-imports.js'; +import '../src/lit-all-imports.js'; +import './column-min-width.common.js'; diff --git a/packages/grid/test/column-min-width-polymer.test.ts b/packages/grid/test/column-min-width-polymer.test.ts new file mode 100644 index 00000000000..c3e261f063a --- /dev/null +++ b/packages/grid/test/column-min-width-polymer.test.ts @@ -0,0 +1,2 @@ +import '../all-imports.js'; +import './column-min-width.common.js'; diff --git a/packages/grid/test/column-min-width.common.ts b/packages/grid/test/column-min-width.common.ts new file mode 100644 index 00000000000..0aa604d9228 --- /dev/null +++ b/packages/grid/test/column-min-width.common.ts @@ -0,0 +1,324 @@ +import { expect } from '@esm-bundle/chai'; +import { fixtureSync, nextRender, oneEvent } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; +import type { Grid } from '../src/vaadin-grid.js'; +import type { GridColumn } from '../src/vaadin-grid-column.js'; +import type { GridColumnGroup } from '../src/vaadin-grid-column-group.js'; +import { fire, flushGrid, getRowCells, getRows, infiniteDataProvider } from './helpers.js'; + +interface ExpectedWidths { + pattern: RegExp; + values: number[]; +} +function expectColumnWidthsToBeOk( + columns: NodeListOf | GridColumnGroup>, + expectedWidths: ExpectedWidths[] = [ + { pattern: /max\((\d+)px, (\d+)px\)/u, values: [50, 71] }, + { pattern: /(\d+)[px]?/u, values: [114] }, + { pattern: /(\d+)[px]?/u, values: [84] }, + { pattern: /(\d+)[px]?/u, values: [107] }, + ], + delta = 5, +) { + // Allowed margin of measurement to keep the test from failing if there are small differences in rendered text + // width on different platforms or if there are small changes to styles which affect horizontal margin/padding. + expectedWidths.forEach((expectedWidth, index) => { + const colWidth = columns[index].width; + expect(colWidth).to.be.not.undefined; + expect(colWidth).to.be.not.null; + const split = colWidth!.split(expectedWidth.pattern); + for (let indexValue = 0; indexValue < expectedWidth.values.length; indexValue++) { + const widthValue: string = split[indexValue + 1]; + const expectedWidthValue = expectedWidth.values[indexValue]; + const columnWidth = parseInt(widthValue); + expect(columnWidth).to.be.closeTo(expectedWidthValue, delta); + } + }); +} + +describe('column auto-width', () => { + let grid: Grid; + let columns: NodeListOf>; + let spy; + + const testItems = [ + { a: 'fubar', b: 'foo', c: 'foo', d: 'a' }, + { a: 'foo', b: 'foo bar baz', c: 'foo', d: 'bar' }, + { a: 'foo', b: 'foo baz', c: 'foo bar', d: 'baz' }, + ]; + + function whenColumnWidthsCalculated(cb: any) { + if ((grid as any)._recalculateColumnWidths.called) { + cb(); + } else { + requestAnimationFrame(() => whenColumnWidthsCalculated(cb)); + } + } + + function recalculateWidths() { + return new Promise((resolve) => { + whenColumnWidthsCalculated(() => { + resolve(); + }); + }); + } + + class TestContainer extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot!.innerHTML = ` + + `; + } + } + + customElements.define('test-container', TestContainer); + + it('should have correct column widths when items are set', async () => { + grid = fixtureSync(` + + `); + spy = sinon.spy(grid, '_recalculateColumnWidths'); + spy.resetHistory(); + columns = grid.querySelectorAll('vaadin-grid-column'); + // Show the grid and wait for animationend event ("vaadin-grid-appear") + // to ensure the grid is in a consistent state before starting each test + grid.hidden = false; + await oneEvent(grid, 'animationend'); + grid.items = testItems; + + await recalculateWidths(); + expectColumnWidthsToBeOk(columns); + }); + + it('should have correct column widths when items are set', async () => { + grid = fixtureSync(` + + `); + spy = sinon.spy(grid, '_recalculateColumnWidths'); + columns = grid.querySelectorAll('vaadin-grid-column'); + // Show the grid and wait for animationend event ("vaadin-grid-appear") + // to ensure the grid is in a consistent state before starting each test + grid.hidden = false; + await oneEvent(grid, 'animationend'); + grid.items = testItems; + await recalculateWidths(); + expectColumnWidthsToBeOk(columns, [{ pattern: /max\((\d+)px, (\d+)px\)/u, values: [150, 151] }]); + }); +}); + +describe('column group', () => { + function createGrid(html: string, items = [{ a: 'm', b: 'mm' }]) { + const grid: Grid = fixtureSync(html); + grid.items = items; + flushGrid(grid); + + return grid; + } + + it('should consider vaadin-grid-column header when calculating column width', () => { + const grid = createGrid(` + + + + + + `); + const columns = grid.querySelectorAll('vaadin-grid-column'); + expectColumnWidthsToBeOk( + columns, + [ + { + pattern: /max\((\d+)px, (\d+)px\)/u, + values: [200, 420], + }, + ], + 25, + ); + const columnGroups = grid.querySelectorAll('vaadin-grid-column-group'); + expectColumnWidthsToBeOk( + columnGroups, + [ + { + pattern: /calc\(max\((\d+)px, (\d+)px\)\)/u, + values: [200, 420], + }, + ], + 25, + ); + }); + it('should consider vaadin-grid-column header when calculating column width', () => { + const grid = createGrid(` + + + + + + + `); + const columnGroups = grid.querySelectorAll('vaadin-grid-column-group'); + expectColumnWidthsToBeOk( + columnGroups, + [ + { + pattern: /calc\(max\((\d+)px, (\d+)px\) \+ max\((\d+)px, (\d+)px\)\)/u, + values: [200, 420, 190, 420], + }, + ], + 25, + ); + }); + it('should consider calculate the width of the group based on the min-width', () => { + const grid = createGrid(` + + + + + + + `); + const columnGroups = grid.querySelectorAll('vaadin-grid-column-group'); + expectColumnWidthsToBeOk( + columnGroups, + [ + { + pattern: /calc\(max\((\d+)px, (\d+)px\) \+ max\((\d+)px, (\d+)px\)\)/u, + values: [200, 201, 190, 191], + }, + ], + 25, + ); + }); + it('should consider ignore the hidden columns', () => { + const grid = createGrid(` + + + + + + + + `); + const columnGroups = grid.querySelectorAll('vaadin-grid-column-group'); + expectColumnWidthsToBeOk( + columnGroups, + [ + { + pattern: /calc\(max\((\d+)px, (\d+)px\) \+ max\((\d+)px, (\d+)px\)\)/u, + values: [200, 201, 190, 191], + }, + ], + 25, + ); + }); +}); + +describe('column resizing', () => { + let grid: Grid, headerCells: NodeListOf, handle: Element; + + beforeEach(async () => { + grid = fixtureSync(` + + + + + `); + grid.querySelectorAll('vaadin-grid-column').forEach((col, idx) => { + col.renderer = (root) => { + root.textContent = idx.toString(); + }; + }); + grid.dataProvider = infiniteDataProvider; + flushGrid(grid); + headerCells = getRowCells(getRows((grid as any).$.header)[0]); + handle = headerCells[0].querySelector('[part~="resize-handle"]')!; + await nextRender(grid); + }); + it('should set min width based on the min width value', () => { + const options = { node: handle }; + const rect = headerCells[0].getBoundingClientRect(); + + expect(headerCells[0].clientWidth).to.be.closeTo(149, 5); + + fire('track', { state: 'start' }, options); + fire('track', { state: 'track', x: rect.left + 130, y: 0 }, options); + expect(headerCells[0].clientWidth).to.be.equal(130); + + fire('track', { state: 'start' }, options); + fire('track', { state: 'track', x: rect.left + 100, y: 0 }, options); + expect(headerCells[0].clientWidth).to.be.equal(100); + }); +}); + +describe('column group resizing', () => { + let grid: Grid; + + beforeEach(async () => { + grid = fixtureSync(` + + + + + + + `); + grid.querySelectorAll('vaadin-grid-column').forEach((col, idx) => { + col.renderer = (root) => { + root.textContent = idx.toString(); + }; + }); + grid.dataProvider = infiniteDataProvider; + flushGrid(grid); + await nextRender(grid); + }); + ['rtl', 'ltr'].forEach((direction) => { + describe(`child columns resizing in ${direction}`, () => { + beforeEach(() => { + grid.setAttribute('dir', direction); + }); + + it('should resize the child column', () => { + const headerRows = getRows((grid as any).$.header); + const groupCell = getRowCells(headerRows[0])[0]; + const handle = groupCell.querySelector('[part~="resize-handle"]'); + + const cell = getRowCells(headerRows[1])[1]; + const rect = cell.getBoundingClientRect(); + const options = { node: handle }; + expect(cell.clientWidth).to.equal(100); + fire('track', { state: 'start' }, options); + fire('track', { state: 'track', x: rect.right + (direction === 'rtl' ? -50 : 50), y: 0 }, options); + + expect(cell.clientWidth).to.equal(direction === 'rtl' ? 70 : 150); + expect(groupCell.clientWidth).to.equal(direction === 'rtl' ? 190 : 270); + }); + + it('should resize the last non-hidden child column', () => { + (grid as any)._columnTree[1][1].hidden = true; + const headerRows = getRows((grid as any).$.header); + const groupCell = getRowCells(headerRows[0])[0]; + const handle = groupCell.querySelector('[part~="resize-handle"]'); + + const cell = getRowCells(headerRows[1])[0]; + const rect = cell.getBoundingClientRect(); + const options = { node: handle }; + fire('track', { state: 'start' }, options); + fire('track', { state: 'track', x: rect.right + (direction === 'rtl' ? -100 : 100), y: 0 }, options); + + expect(cell.clientWidth).to.equal(direction === 'rtl' ? 120 : 220); + expect(groupCell.clientWidth).to.equal(direction === 'rtl' ? 120 : 220); + }); + }); + }); +}); diff --git a/packages/grid/test/typings/grid.types.ts b/packages/grid/test/typings/grid.types.ts index 754b238b408..ea5f1409415 100644 --- a/packages/grid/test/typings/grid.types.ts +++ b/packages/grid/test/typings/grid.types.ts @@ -239,6 +239,7 @@ assertType>(narrowedColumn); assertType(narrowedColumn.flexGrow); assertType(narrowedColumn.width); +assertType(narrowedColumn.minWidth); assertType(narrowedColumn.resizable); assertType(narrowedColumn.frozen); assertType(narrowedColumn.frozenToEnd);