From 0f52b6f2e26f06bbe5b3258eaab495b890a170b4 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Fri, 30 Jan 2026 17:16:33 +0200 Subject: [PATCH 1/4] fix(overlay): conditionally cache element size --- .../core/src/services/overlay/overlay.spec.ts | 16 +++++++++++++--- .../core/src/services/overlay/overlay.ts | 14 +++++++++++--- .../core/src/services/overlay/utilities.ts | 5 +++++ .../notification/notifications.directive.ts | 3 ++- .../tooltip/tooltip-target.directive.ts | 1 + 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts index 2dd45ea03e2..de05d6e0ce7 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -569,7 +569,8 @@ describe('igxOverlay', () => { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false + closeOnEscape: false, + cacheSize: true }; spyOn(overlayInstance.contentAppending, 'emit'); @@ -3506,7 +3507,7 @@ describe('igxOverlay', () => { })); // 4. Css - it('Should use component initial container\'s properties when is with 100% width and show in overlay element', + it('Should use component initial container\'s properties based on cacheSize when it\'s with 100% width and shown in overlay element', fakeAsync(() => { const fixture = TestBed.createComponent(WidthTestOverlayComponent); fixture.detectChanges(); @@ -3525,6 +3526,15 @@ describe('igxOverlay', () => { // content element has no height, so the shown element will calculate its own height by itself // expect(overlayChild.style.height).toEqual('100%'); // expect(overlayChild.getBoundingClientRect().height).toEqual(280); + + fixture.componentInstance.overlaySettings.cacheSize = false; + fixture.componentInstance.buttonElement.nativeElement.click(); + tick(); + const componentElement2 = fixture.componentInstance.customComponent.nativeElement; + expect(componentElement2.style.width).toEqual('100%'); + expect(componentElement2.getBoundingClientRect().width).toEqual(123); + // Check overlay content element width + expect(componentElement2.parentElement.getBoundingClientRect().width).toEqual(123); fixture.componentInstance.overlay.detachAll(); })); }); @@ -4679,7 +4689,7 @@ export class TwoButtonsComponent {
- Some Content +

Some Content

`, styles: [`button { diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 687d0aa143e..07fcd87f03f 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -130,7 +130,8 @@ export class IgxOverlayService implements OnDestroy { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false + closeOnEscape: false, + cacheSize: true }; constructor() { @@ -331,11 +332,18 @@ export class IgxOverlayService implements OnDestroy { info.settings = eventArgs.settings; this._overlayInfos.push(info); info.hook = this.placeElementHook(info.elementRef.nativeElement); - const elementRect = info.elementRef.nativeElement.getBoundingClientRect(); - info.initialSize = { width: elementRect.width, height: elementRect.height }; + let elementRect; + // Get the element rect size before moving it into the overlay to cache its size. + if (info.settings.cacheSize) { + elementRect = info.elementRef.nativeElement.getBoundingClientRect(); + } // Get the size before moving the container into the overlay so that it does not forget about inherited styles. this.getComponentSize(info); this.moveElementToOverlay(info); + if (!info.settings.cacheSize) { + elementRect = info.elementRef.nativeElement.getBoundingClientRect(); + } + info.initialSize = { width: elementRect.width, height: elementRect.height }; // Update the container size after moving if there is size. if (info.size) { info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size); diff --git a/projects/igniteui-angular/core/src/services/overlay/utilities.ts b/projects/igniteui-angular/core/src/services/overlay/utilities.ts index 13a954ce26d..5986231200e 100644 --- a/projects/igniteui-angular/core/src/services/overlay/utilities.ts +++ b/projects/igniteui-angular/core/src/services/overlay/utilities.ts @@ -134,6 +134,11 @@ export interface OverlaySettings { * Clicking on the elements in this collection will not close the overlay when closeOnOutsideClick = true. */ excludeFromOutsideClick?: HTMLElement[]; + /** + * @hidden @internal + * Set if the element should retain its size when moved to the overlay. + */ + cacheSize?: boolean; } export interface OverlayEventArgs extends IBaseEventArgs { diff --git a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts index 0df270be7f5..4d1116ded16 100644 --- a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts @@ -86,7 +86,8 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective closeOnEscape: false, closeOnOutsideClick: false, modal: false, - outlet: this.outlet + outlet: this.outlet, + cacheSize: false }; super.open(overlaySettings); diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index fb6d03c3b76..1b844a183c2 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -400,6 +400,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionSettings); this._overlayDefaults.closeOnOutsideClick = false; this._overlayDefaults.closeOnEscape = true; + this._overlayDefaults.cacheSize = false; this.target.closing.pipe(takeUntil(this._destroy$)).subscribe((event) => { if (this.target.tooltipTarget !== this) { From bbda4dec4a9807c4276b40ce5050db3cacf9b7a4 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Fri, 30 Jan 2026 18:05:49 +0200 Subject: [PATCH 2/4] chore(*): update api docs; add typing --- projects/igniteui-angular/core/src/services/overlay/overlay.ts | 2 +- .../igniteui-angular/core/src/services/overlay/utilities.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 07fcd87f03f..ad90d0381de 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -332,7 +332,7 @@ export class IgxOverlayService implements OnDestroy { info.settings = eventArgs.settings; this._overlayInfos.push(info); info.hook = this.placeElementHook(info.elementRef.nativeElement); - let elementRect; + let elementRect: DOMRect; // Get the element rect size before moving it into the overlay to cache its size. if (info.settings.cacheSize) { elementRect = info.elementRef.nativeElement.getBoundingClientRect(); diff --git a/projects/igniteui-angular/core/src/services/overlay/utilities.ts b/projects/igniteui-angular/core/src/services/overlay/utilities.ts index 5986231200e..8ed0412a20f 100644 --- a/projects/igniteui-angular/core/src/services/overlay/utilities.ts +++ b/projects/igniteui-angular/core/src/services/overlay/utilities.ts @@ -136,7 +136,8 @@ export interface OverlaySettings { excludeFromOutsideClick?: HTMLElement[]; /** * @hidden @internal - * Set if the element should retain its size when moved to the overlay. + * Controls whether element size is measured before (true) or after (false) moving to the overlay container. + * Default is true to retain element size. */ cacheSize?: boolean; } From 5e0e99506aba1cf1674632866b7e3f951a77ac8c Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Tue, 10 Feb 2026 15:52:16 +0200 Subject: [PATCH 3/4] fix(tooltip/snackbar): use overlay size registry --- .../core/src/services/overlay/overlay.spec.ts | 14 +----- .../core/src/services/overlay/overlay.ts | 44 +++++++++++++------ .../core/src/services/overlay/utilities.ts | 38 +++++++++++++--- .../core/src/services/public_api.ts | 2 +- .../notification/notifications.directive.ts | 3 +- .../tooltip/tooltip-target.directive.ts | 1 - .../tooltip/tooltip.directive.spec.ts | 30 ++++++++++++- .../directives/tooltip/tooltip.directive.ts | 24 +++++++++- .../src/snackbar/snackbar.component.spec.ts | 39 +++++++++++++++- .../src/snackbar/snackbar.component.ts | 36 +++++++++++++-- .../test-utils/tooltip-components.spec.ts | 37 +++++++++++++--- 11 files changed, 219 insertions(+), 49 deletions(-) diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts index de05d6e0ce7..8cc3d933e51 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -569,8 +569,7 @@ describe('igxOverlay', () => { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false, - cacheSize: true + closeOnEscape: false }; spyOn(overlayInstance.contentAppending, 'emit'); @@ -3507,7 +3506,7 @@ describe('igxOverlay', () => { })); // 4. Css - it('Should use component initial container\'s properties based on cacheSize when it\'s with 100% width and shown in overlay element', + it('Should use component initial container\'s properties when is with 100% width and show in overlay element', fakeAsync(() => { const fixture = TestBed.createComponent(WidthTestOverlayComponent); fixture.detectChanges(); @@ -3526,15 +3525,6 @@ describe('igxOverlay', () => { // content element has no height, so the shown element will calculate its own height by itself // expect(overlayChild.style.height).toEqual('100%'); // expect(overlayChild.getBoundingClientRect().height).toEqual(280); - - fixture.componentInstance.overlaySettings.cacheSize = false; - fixture.componentInstance.buttonElement.nativeElement.click(); - tick(); - const componentElement2 = fixture.componentInstance.customComponent.nativeElement; - expect(componentElement2.style.width).toEqual('100%'); - expect(componentElement2.getBoundingClientRect().width).toEqual(123); - // Check overlay content element width - expect(componentElement2.parentElement.getBoundingClientRect().width).toEqual(123); fixture.componentInstance.overlay.detachAll(); })); }); diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index ad90d0381de..ee7bc91c916 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -5,7 +5,7 @@ import { filter, takeUntil } from 'rxjs/operators'; import { fadeIn, fadeOut, IAnimationParams, scaleInHorLeft, scaleInHorRight, scaleInVerBottom, scaleInVerTop, scaleOutHorLeft, scaleOutHorRight, scaleOutVerBottom, scaleOutVerTop, slideInBottom, slideInTop, slideOutBottom, slideOutTop } from 'igniteui-angular/animations'; import { PlatformUtil } from '../../core/utils'; -import { IgxOverlayOutletDirective } from './utilities'; +import { IgxOverlayOutletDirective, OverlaySizeRegistry } from './utilities'; import { IgxAngularAnimationService } from '../animation/angular-animation-service'; import { AnimationService } from '../animation/animation'; import { AutoPositionStrategy } from './position/auto-position-strategy'; @@ -44,6 +44,7 @@ export class IgxOverlayService implements OnDestroy { private _zone = inject(NgZone); protected platformUtil = inject(PlatformUtil); private animationService = inject(IgxAngularAnimationService); + private sizeRegistry = inject(OverlaySizeRegistry); /** * Emitted just before the overlay content starts to open. @@ -130,8 +131,7 @@ export class IgxOverlayService implements OnDestroy { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false, - cacheSize: true + closeOnEscape: false }; constructor() { @@ -332,18 +332,12 @@ export class IgxOverlayService implements OnDestroy { info.settings = eventArgs.settings; this._overlayInfos.push(info); info.hook = this.placeElementHook(info.elementRef.nativeElement); - let elementRect: DOMRect; - // Get the element rect size before moving it into the overlay to cache its size. - if (info.settings.cacheSize) { - elementRect = info.elementRef.nativeElement.getBoundingClientRect(); - } // Get the size before moving the container into the overlay so that it does not forget about inherited styles. this.getComponentSize(info); - this.moveElementToOverlay(info); - if (!info.settings.cacheSize) { - elementRect = info.elementRef.nativeElement.getBoundingClientRect(); - } - info.initialSize = { width: elementRect.width, height: elementRect.height }; + this.setInitialSize( + info, + () => this.moveElementToOverlay(info) + ); // Update the container size after moving if there is size. if (info.size) { info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size); @@ -1008,4 +1002,28 @@ export class IgxOverlayService implements OnDestroy { info.size = size; } } + + /** + * Measures the element's initial size and controls *when* the element is moved into the overlay outlet. + * + * The elements inherit constraining parent styles, so + * for some of them (e.g., Tooltip, Snackbar) their pre-move size is incorrect. + * Those can register an override via `OverlaySizeRegistry` to measure **after** moving to get an accurate size. + * + * - **Default**: Measures in-place (current parent), then moves to the overlay. + * + * @param info OverlayInfo for the content being attached. + * @param moveToOverlay Moves the element into the overlay. + */ + private setInitialSize(info: OverlayInfo, moveToOverlay: () => void): void { + const override = this.sizeRegistry.get(info); + if (override) { + override(info, moveToOverlay); + return; + } + + const elementRect = info.elementRef.nativeElement.getBoundingClientRect(); + info.initialSize = { width: elementRect.width, height: elementRect.height }; + moveToOverlay(); + } } diff --git a/projects/igniteui-angular/core/src/services/overlay/utilities.ts b/projects/igniteui-angular/core/src/services/overlay/utilities.ts index 8ed0412a20f..38b45d8631b 100644 --- a/projects/igniteui-angular/core/src/services/overlay/utilities.ts +++ b/projects/igniteui-angular/core/src/services/overlay/utilities.ts @@ -1,10 +1,40 @@ import { AnimationReferenceMetadata } from '@angular/animations'; -import { ComponentRef, Directive, ElementRef, inject, Injector, NgZone } from '@angular/core'; +import { ComponentRef, Directive, ElementRef, inject, Injectable, Injector, NgZone } from '@angular/core'; import { CancelableBrowserEventArgs, CancelableEventArgs, cloneValue, IBaseEventArgs } from '../../core/utils'; import { AnimationPlayer } from '../animation/animation'; import { IPositionStrategy } from './position/IPositionStrategy'; import { IScrollStrategy } from './scroll'; +type SetInitialSizeFn = (info: OverlayInfo, moveToOverlay: () => void) => void; + +/** + * Maps a host `HTMLElement` to a sizing strategy (`SetInitialSizeFn`). + * + * @hidden + * @internal + */ + +@Injectable({ providedIn: 'root' }) +export class OverlaySizeRegistry { + private readonly map = new Map(); + + public register(host: HTMLElement, fn: SetInitialSizeFn): void { + this.map.set(host, fn); + } + + public clear(host: HTMLElement): void { + this.map.delete(host); + } + + public get(info: OverlayInfo): SetInitialSizeFn | undefined { + if (!info.elementRef || !info.elementRef.nativeElement) { + return; + } + + return this.map.get(info.elementRef.nativeElement); + } +} + /** * Mark an element as an igxOverlay outlet container. * Directive instance is exported as `overlay-outlet` to be assigned to templates variables: @@ -134,12 +164,6 @@ export interface OverlaySettings { * Clicking on the elements in this collection will not close the overlay when closeOnOutsideClick = true. */ excludeFromOutsideClick?: HTMLElement[]; - /** - * @hidden @internal - * Controls whether element size is measured before (true) or after (false) moving to the overlay container. - * Default is true to retain element size. - */ - cacheSize?: boolean; } export interface OverlayEventArgs extends IBaseEventArgs { diff --git a/projects/igniteui-angular/core/src/services/public_api.ts b/projects/igniteui-angular/core/src/services/public_api.ts index 69ae76e9358..498bda162d6 100644 --- a/projects/igniteui-angular/core/src/services/public_api.ts +++ b/projects/igniteui-angular/core/src/services/public_api.ts @@ -9,7 +9,7 @@ export * from './overlay/scroll'; export { AbsolutePosition, ConnectedFit, HorizontalAlignment, OffsetMode, OverlayAnimationEventArgs, OverlayCancelableEventArgs, OverlayClosingEventArgs, OverlayCreateSettings, OverlayEventArgs, OverlaySettings, Point, PositionSettings, RelativePosition, RelativePositionStrategy, Size, VerticalAlignment, Util, - IgxOverlayOutletDirective + IgxOverlayOutletDirective, OverlayInfo, OverlaySizeRegistry } from './overlay/utilities'; export * from './transaction/base-transaction'; export * from './transaction/hierarchical-transaction'; diff --git a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts index 4d1116ded16..0df270be7f5 100644 --- a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts @@ -86,8 +86,7 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective closeOnEscape: false, closeOnOutsideClick: false, modal: false, - outlet: this.outlet, - cacheSize: false + outlet: this.outlet }; super.open(overlaySettings); diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index 1b844a183c2..fb6d03c3b76 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -400,7 +400,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionSettings); this._overlayDefaults.closeOnOutsideClick = false; this._overlayDefaults.closeOnEscape = true; - this._overlayDefaults.cacheSize = false; this.target.closing.pipe(takeUntil(this._destroy$)).subscribe((event) => { if (this.target.tooltipTarget !== this) { diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts index 1846182d6de..b35ed1c4fd0 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts @@ -2,7 +2,7 @@ import { DebugElement } from '@angular/core'; import { fakeAsync, TestBed, tick, flush, waitForAsync, ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, IgxTooltipMultipleTooltipsComponent, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, IgxTooltipNestedTooltipsComponent } from '../../../../test-utils/tooltip-components.spec'; +import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, IgxTooltipMultipleTooltipsComponent, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, IgxTooltipNestedTooltipsComponent, IgxTooltipSizeComponent } from '../../../../test-utils/tooltip-components.spec'; import { UIInteractions } from '../../../../test-utils/ui-interactions.spec'; import { HorizontalAlignment, VerticalAlignment, AutoPositionStrategy } from '../../../../core/src/services/public_api'; import { IgxTooltipDirective } from './tooltip.directive'; @@ -30,7 +30,8 @@ describe('IgxTooltip', () => { IgxTooltipWithToggleActionComponent, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, - IgxTooltipNestedTooltipsComponent + IgxTooltipNestedTooltipsComponent, + IgxTooltipSizeComponent ] }).compileComponents(); UIInteractions.clearOverlay(); @@ -996,6 +997,31 @@ describe('IgxTooltip', () => { expect(fix.componentInstance.toggleDir.collapsed).toBe(false); })); + + it('correctly sizes the tooltip/overlay content when inside an element - issue #16458', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxTooltipSizeComponent); + fixture.detectChanges(); + + fixture.componentInstance.target1.showTooltip(); + fixture.componentInstance.target2.showTooltip(); + fixture.componentInstance.target3.showTooltip(); + flush(); + + const tooltip1Rect = fixture.componentInstance.tooltip1.element.getBoundingClientRect(); + const tooltip2Rect = fixture.componentInstance.tooltip2.element.getBoundingClientRect(); + const tooltip3Rect = fixture.componentInstance.tooltip3.element.getBoundingClientRect(); + + const tooltip1ParentRect = fixture.componentInstance.tooltip1.element.parentElement.getBoundingClientRect(); + const tooltip2ParentRect = fixture.componentInstance.tooltip2.element.parentElement.getBoundingClientRect(); + const tooltip3ParentRect = fixture.componentInstance.tooltip3.element.parentElement.getBoundingClientRect(); + + expect(tooltip1Rect.width).toEqual(tooltip1ParentRect.width); + expect(tooltip1Rect.height).toEqual(tooltip1ParentRect.height); + expect(tooltip2Rect.width).toEqual(tooltip2ParentRect.width); + expect(tooltip2Rect.height).toEqual(tooltip2ParentRect.height); + expect(tooltip3Rect.width).toEqual(tooltip3ParentRect.width); + expect(tooltip3Rect.height).toEqual(tooltip3ParentRect.height); + })); }); describe('Tooltip Sticky with Close Button', () => { diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.ts index 626dab2ad25..57950708227 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.ts @@ -3,8 +3,9 @@ import { OnDestroy, inject, DOCUMENT, HostListener, Renderer2, AfterViewInit, + OnInit, } from '@angular/core'; -import { OverlaySettings, PlatformUtil } from 'igniteui-angular/core'; +import { OverlayInfo, OverlaySettings, OverlaySizeRegistry, PlatformUtil } from 'igniteui-angular/core'; import { IgxToggleDirective } from '../toggle/toggle.directive'; import { IgxTooltipTargetDirective } from './tooltip-target.directive'; import { Subject, takeUntil } from 'rxjs'; @@ -29,7 +30,7 @@ let NEXT_ID = 0; selector: '[igxTooltip]', standalone: true }) -export class IgxTooltipDirective extends IgxToggleDirective implements AfterViewInit, OnDestroy { +export class IgxTooltipDirective extends IgxToggleDirective implements OnInit, AfterViewInit, OnDestroy { /** * @hidden */ @@ -119,6 +120,7 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView private _document = inject(DOCUMENT); private _renderer = inject(Renderer2); private _platformUtil = inject(PlatformUtil); + private _sizeRegistry = inject(OverlaySizeRegistry); /** @hidden */ constructor() { @@ -133,6 +135,12 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView }); } + /** @hidden */ + public override ngOnInit() { + super.ngOnInit(); + this._sizeRegistry.register(this.element, this.setInitialSize); + } + /** @hidden */ public ngAfterViewInit(): void { if (this._platformUtil.isBrowser) { @@ -151,6 +159,8 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView if (this.arrow) { this._removeArrow(); } + + this._sizeRegistry.clear(this.element); } /** @@ -226,4 +236,14 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView private onDocumentTouchStart(event) { this.tooltipTarget?.onDocumentTouchStart(event); } + + /** + * Measures **after** moving the element into the overlay outlet so that parent + * style constraints do not affect the initial size. + */ + private setInitialSize = (info: OverlayInfo, moveToOverlay: () => void) => { + moveToOverlay(); + const elementRect = info.elementRef.nativeElement.getBoundingClientRect(); + info.initialSize = { width: elementRect.width, height: elementRect.height }; + } } diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts index d8cf22d0adb..01bdb10e8ea 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts @@ -14,7 +14,8 @@ describe('IgxSnackbar', () => { imports: [ NoopAnimationsModule, SnackbarInitializeTestComponent, - SnackbarCustomContentComponent + SnackbarCustomContentComponent, + SnackbarSizeTestComponent ] }).compileComponents(); })); @@ -183,6 +184,28 @@ describe('IgxSnackbar', () => { expect(customPositionSettings.openAnimation.options.params).toEqual({duration: '1000ms'}); expect(customPositionSettings.minSize).toEqual({height: 100, width: 100}); }); + + it('correctly sizes the snackbar/overlay content when inside an element - issue #16458', () => { + const fix = TestBed.createComponent(SnackbarSizeTestComponent); + fix.detectChanges(); + snackbar = fix.componentInstance.snackbar; + + const parentDivRect = snackbar.element.parentElement.getBoundingClientRect(); + expect(parentDivRect.width).toBe(600); + + snackbar.open(); + fix.detectChanges(); + + const snackbarRect = snackbar.element.getBoundingClientRect(); + const overlayContentRect = snackbar.element.parentElement.getBoundingClientRect(); + const { marginLeft, marginRight, paddingLeft, paddingRight } = getComputedStyle(snackbar.element); + const horizontalMargins = parseFloat(marginLeft) + parseFloat(marginRight); + const horizontalPaddings = parseFloat(paddingLeft) + parseFloat(paddingRight); + const contentWidth = 200; + + expect(snackbarRect.width).toEqual(contentWidth + horizontalPaddings); + expect(overlayContentRect.width).toEqual(snackbarRect.width + horizontalMargins); + }); }); describe('IgxSnackbar with custom content', () => { @@ -273,3 +296,17 @@ class SnackbarCustomContentComponent { @ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent; public text: string; } + +@Component({ + template: ` +
+ +
Snackbar Message
+
+
+ `, + imports: [IgxSnackbarComponent] +}) +class SnackbarSizeTestComponent { + @ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent; +} diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts index cd7b50468fe..37aeed85ab0 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts @@ -1,15 +1,19 @@ import { useAnimation } from '@angular/animations'; import { Component, + DOCUMENT, EventEmitter, HostBinding, + inject, Input, + OnDestroy, OnInit, Output } from '@angular/core'; import { takeUntil } from 'rxjs/operators'; import { ContainerPositionStrategy, GlobalPositionStrategy, HorizontalAlignment, - PositionSettings, VerticalAlignment } from 'igniteui-angular/core'; + OverlayInfo, + OverlaySizeRegistry, PositionSettings, VerticalAlignment } from 'igniteui-angular/core'; import { ToggleViewEventArgs, IgxButtonDirective, IgxNotificationsDirective } from 'igniteui-angular/directives'; import { fadeIn, fadeOut } from 'igniteui-angular/animations'; @@ -36,8 +40,10 @@ let NEXT_ID = 0; templateUrl: 'snackbar.component.html', imports: [IgxButtonDirective] }) -export class IgxSnackbarComponent extends IgxNotificationsDirective - implements OnInit { +export class IgxSnackbarComponent extends IgxNotificationsDirective implements OnInit, OnDestroy { + private _document = inject(DOCUMENT); + private _sizeRegistry = inject(OverlaySizeRegistry); + /** * Sets/gets the `id` of the snackbar. * If not set, the `id` of the first snackbar component will be `"igx-snackbar-0"`; @@ -196,5 +202,29 @@ export class IgxSnackbarComponent extends IgxNotificationsDirective const closedEventArgs: ToggleViewEventArgs = { owner: this, id: this._overlayId }; this.animationDone.emit(closedEventArgs); }); + + this._sizeRegistry.register(this.element, this.setInitialSize); + } + + /** + * @hidden + */ + public override ngOnDestroy() { + super.ngOnDestroy(); + this._sizeRegistry.clear(this.element); + } + + /** + * Measures **after** moving the element into the overlay outlet so that parent + * style constraints do not affect the initial size. + */ + private setInitialSize = (info: OverlayInfo, moveToOverlay: () => void) => { + moveToOverlay(); + const elementRect = info.elementRef.nativeElement.getBoundingClientRect(); + // Needs full element width (margins included) to set proper width for the overlay container. + // Otherwise, the snackbar appears smaller and the text inside it might be misaligned. + const styles = this._document.defaultView.getComputedStyle(info.elementRef.nativeElement); + const horizontalMargins = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight); + info.initialSize = { width: elementRect.width + horizontalMargins, height: elementRect.height }; } } diff --git a/projects/igniteui-angular/test-utils/tooltip-components.spec.ts b/projects/igniteui-angular/test-utils/tooltip-components.spec.ts index 01f45faf660..d44881471ff 100644 --- a/projects/igniteui-angular/test-utils/tooltip-components.spec.ts +++ b/projects/igniteui-angular/test-utils/tooltip-components.spec.ts @@ -1,5 +1,6 @@ import { Component, TemplateRef, ViewChild } from '@angular/core'; -import { IgxToggleActionDirective, IgxToggleDirective, IgxTooltipDirective, IgxTooltipTargetDirective, ITooltipHideEventArgs, ITooltipShowEventArgs } from 'igniteui-angular/directives'; +import { IgxButtonDirective, IgxIconButtonDirective, IgxToggleActionDirective, IgxToggleDirective, IgxTooltipDirective, IgxTooltipTargetDirective, ITooltipHideEventArgs, ITooltipShowEventArgs } from 'igniteui-angular/directives'; +import { IgxIconComponent } from 'igniteui-angular/icon'; @Component({ @@ -161,8 +162,7 @@ export class IgxTooltipWithCloseButtonComponent { `, - imports: [IgxTooltipDirective, IgxTooltipTargetDirective], - standalone: true + imports: [IgxTooltipDirective, IgxTooltipTargetDirective] }) export class IgxTooltipWithNestedContentComponent { @ViewChild(IgxTooltipDirective, { static: true }) public tooltip!: IgxTooltipDirective; @@ -195,8 +195,7 @@ export class IgxTooltipWithNestedContentComponent { `, - imports: [IgxTooltipDirective, IgxTooltipTargetDirective], - standalone: true + imports: [IgxTooltipDirective, IgxTooltipTargetDirective] }) export class IgxTooltipNestedTooltipsComponent { @ViewChild('targetLevel1', { read: IgxTooltipTargetDirective, static: true }) public targetLevel1: IgxTooltipTargetDirective; @@ -207,3 +206,31 @@ export class IgxTooltipNestedTooltipsComponent { @ViewChild('tooltipLevel2', { read: IgxTooltipDirective, static: true }) public tooltipLevel2: IgxTooltipDirective; @ViewChild('tooltipLevel3', { read: IgxTooltipDirective, static: true }) public tooltipLevel3: IgxTooltipDirective; } + +@Component({ + template: ` +
+
{{ message }}
+
+ + + `, + imports: [IgxTooltipDirective, IgxTooltipTargetDirective, IgxIconComponent, IgxIconButtonDirective, IgxButtonDirective] +}) +export class IgxTooltipSizeComponent { + @ViewChild('target1', { read: IgxTooltipTargetDirective, static: true }) public target1: IgxTooltipTargetDirective; + @ViewChild('target2', { read: IgxTooltipTargetDirective, static: true }) public target2: IgxTooltipTargetDirective; + @ViewChild('target3', { read: IgxTooltipTargetDirective, static: true }) public target3: IgxTooltipTargetDirective; + + @ViewChild('tooltip1', { read: IgxTooltipDirective, static: true }) public tooltip1: IgxTooltipDirective; + @ViewChild('tooltip2', { read: IgxTooltipDirective, static: true }) public tooltip2: IgxTooltipDirective; + @ViewChild('tooltip3', { read: IgxTooltipDirective, static: true }) public tooltip3: IgxTooltipDirective; + + public message: string = 'Long tooltip message for testing purposes'; +} From 0de0c4ae777d0d3142288d8edd7c877a091cb940 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Tue, 10 Feb 2026 16:16:12 +0200 Subject: [PATCH 4/4] test(overlay): remove previous test setup --- .../igniteui-angular/core/src/services/overlay/overlay.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts index 8cc3d933e51..2dd45ea03e2 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -4679,7 +4679,7 @@ export class TwoButtonsComponent {
-

Some Content

+ Some Content
`, styles: [`button {