From ad3c2314d93ee66a7e20b711459255cdc3dbfd2d Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 4 Mar 2025 16:11:57 +0200 Subject: [PATCH 1/4] feat(igxGrid): Apply min/max width constraints on user-set and auto-widths. --- .../src/lib/grids/columns/column.component.ts | 52 +++- .../src/lib/grids/grid-base.directive.ts | 8 +- .../src/lib/grids/grid/grid.component.spec.ts | 250 +++++++++++++++++- 3 files changed, 299 insertions(+), 11 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 281b6ad7d93..5ee01fa792e 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -523,8 +523,15 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy */ @WatchColumnChanges() @Input() - public maxWidth: string; + public set maxWidth(value: string) { + this._maxWidth = value; + this.grid.notifyChanges(true); + this.grid.cdr.detectChanges(); + } + public get maxWidth(): string { + return this._maxWidth; + } /** * Sets/gets the class selector of the column header. * ```typescript @@ -968,7 +975,8 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy return; } this._defaultMinWidth = value; - + this.grid.notifyChanges(true); + this.grid.cdr.detectChanges(); } public get minWidth(): string { return !this._defaultMinWidth ? this.defaultMinWidth : this._defaultMinWidth; @@ -1732,6 +1740,11 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy */ public destroy$ = new Subject(); + /** + * @hidden + */ + public widthConstrained = false; + /** * @hidden */ @@ -1806,6 +1819,10 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy * @hidden */ protected _defaultMinWidth = ''; + /** + * @hidden + */ + protected _maxWidth = ''; /** * @hidden */ @@ -2091,7 +2108,8 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy if (size && !!size.width) { result.push(size.width + 'px'); } else { - result.push(parseFloat(this.grid.getPossibleColumnWidth()) + 'px'); + const currentWidth = parseFloat(this.grid.getPossibleColumnWidth()); + result.push((this.getConstrainedSizePx(currentWidth)) + 'px'); } } return result; @@ -2550,6 +2568,23 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy return res.join(' '); } + /** + * @hidden + * @internal + */ + public getConstrainedSizePx(newSize){ + if (this.maxWidth && newSize > this.maxWidthPx) { + this.widthConstrained = true; + return this.maxWidthPx; + } else if (this.minWidth && newSize < this.minWidthPx) { + this.widthConstrained = true; + return this.minWidthPx; + } else { + this.widthConstrained = false; + return newSize; + } + } + /** * @hidden * @internal @@ -2559,14 +2594,17 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; const isAutoWidth = colWidth && typeof colWidth === 'string' && colWidth === 'fit-content'; if (isPercentageWidth && this.grid.isColumnWidthSum) { - this._calcWidth = this.grid.minColumnWidth; + this._calcWidth = this.minWidthPx ?? this.grid.minColumnWidth; } else if (isPercentageWidth) { - this._calcWidth = parseFloat(colWidth) / 100 * this.grid.calcWidth; + const currentCalcWidth = parseFloat(colWidth) / 100 * this.grid.calcWidth; + this._calcWidth = this.grid.calcWidth ? this.getConstrainedSizePx(currentCalcWidth) : 0; } else if (!colWidth || isAutoWidth && !this.autoSize) { // no width - this._calcWidth = this.defaultWidth || this.grid.getPossibleColumnWidth(); + const currentCalcWidth = this.defaultWidth || this.grid.getPossibleColumnWidth(); + this._calcWidth = this.getConstrainedSizePx(currentCalcWidth); } else { - this._calcWidth = this.width; + const currentCalcWidth = parseFloat(this.width); + this._calcWidth =this.getConstrainedSizePx(currentCalcWidth); } this.calcPixelWidth = parseFloat(this._calcWidth); } diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 3051f5657ef..0ce8071dba6 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -5416,14 +5416,14 @@ export abstract class IgxGridBaseDirective implements GridType, const columnsWithSetWidths = this.hasColumnLayouts ? visibleCols.filter(c => c.widthSetByUser) : - visibleChildColumns.filter(c => c.widthSetByUser && c.width !== 'fit-content'); + visibleChildColumns.filter(c => (c.widthSetByUser || c.widthConstrained) && c.width !== 'fit-content'); const columnsToSize = this.hasColumnLayouts ? combinedBlocksSize - columnsWithSetWidths.length : visibleChildColumns.length - columnsWithSetWidths.length; const sumExistingWidths = columnsWithSetWidths .reduce((prev, curr) => { - const colWidth = curr.width; + const colWidth = !curr.widthConstrained ? curr.width : curr.calcPixelWidth; let widthValue = parseFloat(colWidth); if (isNaN(widthValue)) { widthValue = MINIMUM_COLUMN_WIDTH; @@ -5431,7 +5431,9 @@ export abstract class IgxGridBaseDirective implements GridType, const currWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1 ? widthValue / 100 * computedWidth : widthValue; - return prev + currWidth; + // apply constraints, since constraint may change width + const constrainedWidth = curr.getConstrainedSizePx(currWidth); + return prev + constrainedWidth; }, 0); // When all columns are hidden, return 0px width diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts index 77856d7b7e1..e08ee298b29 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts @@ -2007,6 +2007,254 @@ describe('IgxGrid Component Tests #grid', () => { })); }); + describe('IgxGrid - min/max width constraints rules', () => { + beforeAll(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + IgxGridDefaultRenderingComponent + ] + }) + .compileComponents(); + })); + + describe('min/max in px', () => { + + it('in column with no width should not go outside bounds.', async() => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + const col2 = grid.columns[1]; + const col3 = grid.columns[2]; + const col4 = grid.columns[3]; + + // without constraint, they split width equally + expect(col1.calcPixelWidth).toBe(grid.calcWidth / 4); + expect(col2.calcPixelWidth).toBe(grid.calcWidth / 4); + expect(col3.calcPixelWidth).toBe(grid.calcWidth / 4); + expect(col4.calcPixelWidth).toBe(grid.calcWidth / 4); + + // set smaller max in px + col1.maxWidth = '100px'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(100); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + + col1.minWidth = '600px'; + fix.detectChanges(); + await wait(16); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(600); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + }); + + it('in column with pixel width should not go outside bounds.', async() => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.detectChanges(); + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + col1.width = "150px"; + fix.detectChanges(); + + expect(col1.calcPixelWidth).toBe(150); + + // set smaller max in px + col1.maxWidth = '100px'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(100); + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + col1.minWidth = '500px'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(500); + }); + + it('in column with auto width should not go outside bounds.', async() => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.componentInstance.columns[0].header = "Some longer text to auto-size"; + fix.componentInstance.columns[0].width = 'auto'; + fix.detectChanges(); + // wait for auto-sizing + await wait(100); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + + // some autosize should be calculated + expect(col1.autoSize).not.toBeUndefined(); + + // set smaller max in px + col1.maxWidth = '100px'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(100); + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + col1.minWidth = '500px'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(500); + }); + }); + + + describe('min/max in %', () => { + it('in column with no width should not go outside bounds.', async () => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + const col2 = grid.columns[1]; + const col3 = grid.columns[2]; + const col4 = grid.columns[3]; + + // set smaller max in % + col1.maxWidth = '10%'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.1); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + col1.minWidth = '50%'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.5); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + }); + + it('in column with pixel width should not go outside bounds.', async() => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.componentInstance.columns[0].width = '400px'; + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + const col2 = grid.columns[1]; + const col3 = grid.columns[2]; + const col4 = grid.columns[3]; + + // set smaller max in % + col1.maxWidth = '10%'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.1); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + col1.minWidth = '50%'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.5); + // the rest split the remaining width + expect(col2.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col3.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + expect(col4.calcPixelWidth).toBe((grid.calcWidth - col1.calcPixelWidth) / 3); + }); + + it('in column with auto width should not go outside bounds.', async() => { + const fix = TestBed.createComponent(IgxGridDefaultRenderingComponent); + // 4 cols + fix.componentInstance.initColumnsRows(5, 4); + fix.componentInstance.columns[0].header = "Some longer text to auto-size"; + fix.componentInstance.columns[0].width = 'auto'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + + const grid = fix.componentInstance.grid; + const col1 = grid.columns[0]; + + // some autosize should be calculated + expect(col1.autoSize).not.toBeUndefined(); + + // set smaller max in px + col1.maxWidth = '10%'; + fix.detectChanges(); + + // first column takes new max + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.1); + + // set larger min in px + col1.maxWidth = null; + fix.detectChanges(); + col1.minWidth = '50%'; + fix.detectChanges(); + await wait(100); + fix.detectChanges(); + + // first column takes new min + expect(col1.calcPixelWidth).toBe(grid.calcWidth * 0.5); + }); + }) + + }); + describe('IgxGrid - API methods', () => { beforeAll(waitForAsync(() => { TestBed.configureTestingModule({ @@ -3001,7 +3249,7 @@ export class IgxGridTestComponent { @Component({ template: ` - + `, From 4ef52bbcce59048cbe3188c5eefcc6b5b5d12f7e Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 4 Mar 2025 16:43:39 +0200 Subject: [PATCH 2/4] chore(*): Additional fix for columnLayouts. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 26c61c72ee6..3758f8f64f7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -5427,7 +5427,8 @@ export abstract class IgxGridBaseDirective implements GridType, visibleChildColumns.length - columnsWithSetWidths.length; const sumExistingWidths = columnsWithSetWidths .reduce((prev, curr) => { - const colWidth = !curr.widthConstrained ? curr.width : curr.calcPixelWidth; + const colInstance = this.hasColumnLayouts ? curr.ref : curr; + const colWidth = !colInstance.widthConstrained ? curr.width : colInstance.calcPixelWidth; let widthValue = parseFloat(colWidth); if (isNaN(widthValue)) { widthValue = MINIMUM_COLUMN_WIDTH; @@ -5436,7 +5437,7 @@ export abstract class IgxGridBaseDirective implements GridType, widthValue / 100 * computedWidth : widthValue; // apply constraints, since constraint may change width - const constrainedWidth = curr.getConstrainedSizePx(currWidth); + const constrainedWidth = this.hasColumnLayouts ? currWidth : colInstance.getConstrainedSizePx(currWidth); return prev + constrainedWidth; }, 0); From b423e48ee8da98678af3692afd05c04c8fef9284 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 6 Mar 2025 18:34:02 +0200 Subject: [PATCH 3/4] chore(*): Apply only user-set min-width as initial constraint. --- .../src/lib/grids/columns/column.component.ts | 17 +++++++++++++---- .../src/lib/grids/grid-base.directive.ts | 6 +++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 62f8f02fdc6..2a81552b53c 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -944,6 +944,15 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy return isPercentageWidth ? parseFloat(this.minWidth) / 100 * gridAvailableSize : parseFloat(this.minWidth); } + /** + * @hidden + */ + public get userSetMinWidthPx() { + const gridAvailableSize = this.grid.calcWidth; + const isPercentageWidth = this._defaultMinWidth && typeof this._defaultMinWidth === 'string' && this._defaultMinWidth.indexOf('%') !== -1; + return isPercentageWidth ? parseFloat(this._defaultMinWidth) / 100 * gridAvailableSize : parseFloat(this._defaultMinWidth); + } + /** * @hidden */ @@ -1822,7 +1831,7 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy /** * @hidden */ - protected _maxWidth = ''; + protected _maxWidth; /** * @hidden */ @@ -2579,9 +2588,9 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy if (this.maxWidth && newSize > this.maxWidthPx) { this.widthConstrained = true; return this.maxWidthPx; - } else if (this.minWidth && newSize < this.minWidthPx) { + } else if (this.minWidth && newSize < this.userSetMinWidthPx) { this.widthConstrained = true; - return this.minWidthPx; + return this.userSetMinWidthPx; } else { this.widthConstrained = false; return newSize; @@ -2597,7 +2606,7 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; const isAutoWidth = colWidth && typeof colWidth === 'string' && colWidth === 'fit-content'; if (isPercentageWidth && this.grid.isColumnWidthSum) { - this._calcWidth = this.minWidthPx ?? this.grid.minColumnWidth; + this._calcWidth = this.userSetMinWidthPx ? this.userSetMinWidthPx : this.grid.minColumnWidth; } else if (isPercentageWidth) { const currentCalcWidth = parseFloat(colWidth) / 100 * this.grid.calcWidth; this._calcWidth = this.grid.calcWidth ? this.getConstrainedSizePx(currentCalcWidth) : 0; diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 3758f8f64f7..6793e2b3b7c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -6554,7 +6554,7 @@ export abstract class IgxGridBaseDirective implements GridType, if (width && typeof width !== 'string') { width = String(width); } - const minWidth = width.indexOf('%') === -1 ? column.minWidthPx : column.minWidthPercent; + const minWidth = width.indexOf('%') === -1 ? column.userSetMinWidthPx : column.minWidthPercent; const maxWidth = width.indexOf('%') === -1 ? column.maxWidthPx : column.maxWidthPercent; if (column.hidden) { return width; @@ -7336,8 +7336,8 @@ export abstract class IgxGridBaseDirective implements GridType, let maxSize = Math.ceil(Math.max(...cellsContentWidths)) + 1; if (col.maxWidth && maxSize > col.maxWidthPx) { maxSize = col.maxWidthPx; - } else if (maxSize < col.minWidthPx) { - maxSize = col.minWidthPx; + } else if (maxSize < col.userSetMinWidthPx) { + maxSize = col.userSetMinWidthPx; } col.autoSize = maxSize; col.resetCaches(); From 81d1d2d34e6ba70be532f4fef69bebbb45a83f7f Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 7 Mar 2025 15:25:40 +0200 Subject: [PATCH 4/4] chore(*): Update test according to new spec. Remove unneccesary change detect. --- .../igniteui-angular/src/lib/grids/columns/column.component.ts | 2 -- projects/igniteui-angular/src/lib/grids/grid/column.spec.ts | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 2a81552b53c..bd1e961c7e0 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -527,7 +527,6 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy this._maxWidth = value; this.grid.notifyChanges(true); - this.grid.cdr.detectChanges(); } public get maxWidth(): string { return this._maxWidth; @@ -985,7 +984,6 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy } this._defaultMinWidth = value; this.grid.notifyChanges(true); - this.grid.cdr.detectChanges(); } public get minWidth(): string { return !this._defaultMinWidth ? this.defaultMinWidth : this._defaultMinWidth; diff --git a/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts index b63728b1b98..7f4ee5f57d5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts @@ -1564,7 +1564,8 @@ describe('IgxGrid - Column properties #grid', () => { tick(); let widths = grid.columns.map(x => x.width); - expect(widths).toEqual(['80px', '130px', '121px', '114px', '92px', '80px', '86px', '108px', '82px', '80px']); + // default min of 80px is disregarded for user-set widths, including auto. + expect(widths).toEqual(['68px', '130px', '121px', '114px', '92px', '72px', '86px', '108px', '82px', '69px']); fix.componentInstance.data = SampleTestData.contactInfoData(); fix.detectChanges(); tick();