From 1aa09240e968d5a1d6762507404a4fa8e75a7d29 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Fri, 30 Jan 2026 18:29:42 +0200 Subject: [PATCH 1/3] fix(overlay): conditionally cache element size --- .../notification/notifications.directive.ts | 3 ++- .../tooltip/tooltip-target.directive.ts | 1 + .../src/lib/services/overlay/overlay.spec.ts | 16 +++++++++++++--- .../src/lib/services/overlay/overlay.ts | 14 +++++++++++--- .../src/lib/services/overlay/utilities.ts | 6 ++++++ 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts b/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts index 5401cbe49dc..a523c8cd9da 100644 --- a/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/src/lib/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/src/lib/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts index 33a59cbf661..435933102ef 100644 --- a/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts @@ -407,6 +407,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) { diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts index 65fbdebe537..1a553f153b8 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts @@ -613,7 +613,8 @@ describe('igxOverlay', () => { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false + closeOnEscape: false, + cacheSize: true }; spyOn(overlayInstance.contentAppending, 'emit'); @@ -3549,7 +3550,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(); @@ -3568,6 +3569,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(); })); }); @@ -4723,7 +4733,7 @@ export class TwoButtonsComponent {
- Some Content +

Some Content

`, styles: [`button { diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index e8e511fb15c..bdf1456a601 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -138,7 +138,8 @@ export class IgxOverlayService implements OnDestroy { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false + closeOnEscape: false, + cacheSize: true }; constructor( @@ -344,11 +345,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: 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 }; // 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/src/lib/services/overlay/utilities.ts b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts index 3286909a8f2..18d4ab968c8 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts @@ -113,6 +113,12 @@ 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 { From 85f5467d93c3efcbfb2214ad75dfd33a72deefe9 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Mon, 16 Feb 2026 09:54:56 +0200 Subject: [PATCH 2/3] fix(tooltip/snackbar): use add overlay size registry --- .../date-range-picker.component.spec.ts | 5 ++- .../notification/notifications.directive.ts | 3 +- .../tooltip/tooltip-target.directive.ts | 1 - .../tooltip/tooltip.directive.spec.ts | 30 ++++++++++++- .../directives/tooltip/tooltip.directive.ts | 24 +++++++++- .../src/lib/services/overlay/overlay.spec.ts | 21 +++------ .../src/lib/services/overlay/overlay.ts | 44 ++++++++++++++----- .../src/lib/services/overlay/utilities.ts | 38 +++++++++++++--- .../lib/snackbar/snackbar.component.spec.ts | 39 +++++++++++++++- .../src/lib/snackbar/snackbar.component.ts | 34 +++++++++++++- .../lib/test-utils/tooltip-components.spec.ts | 31 +++++++++++++ 11 files changed, 226 insertions(+), 44 deletions(-) diff --git a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts index 2f6871b8e29..1059bee4572 100644 --- a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts +++ b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts @@ -26,6 +26,7 @@ import { registerLocaleData } from "@angular/common"; import localeJa from "@angular/common/locales/ja"; import localeBg from "@angular/common/locales/bg"; import { CalendarDay } from '../calendar/common/model'; +import { OverlaySizeRegistry } from '../services/overlay/utilities'; // The number of milliseconds in one day const DEBOUNCE_TIME = 16; @@ -64,6 +65,7 @@ describe('IgxDateRangePicker', () => { let mockCalendar: IgxCalendarComponent; let mockDaysView: any; let mockAnimationService: AnimationService; + let mockOverlaySizeRegistry: OverlaySizeRegistry; let mockCdr: any; const elementRef = { nativeElement: null }; const platform = {} as any; @@ -134,8 +136,9 @@ describe('IgxDateRangePicker', () => { mockNgZone = {}; mockPlatformUtil = { isIOS: false }; mockAnimationService = new IgxAngularAnimationService(mockAnimationBuilder); + mockOverlaySizeRegistry = new OverlaySizeRegistry(); overlay = new IgxOverlayService( - mockApplicationRef, mockDocument, mockNgZone, mockPlatformUtil, mockAnimationService); + mockApplicationRef, mockDocument, mockNgZone, mockPlatformUtil, mockAnimationService, mockOverlaySizeRegistry); mockCalendar = new IgxCalendarComponent(platform, 'en'); mockDaysView = { diff --git a/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts b/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts index a523c8cd9da..5401cbe49dc 100644 --- a/projects/igniteui-angular/src/lib/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/src/lib/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/src/lib/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts index 435933102ef..33a59cbf661 100644 --- a/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts @@ -407,7 +407,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/src/lib/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.spec.ts index 65dd9951b82..54142f87964 100644 --- a/projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/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 '../../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/src/lib/directives/tooltip/tooltip.directive.ts b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.ts index e04f71ba426..54d23af6f12 100644 --- a/projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.ts @@ -3,9 +3,10 @@ import { OnDestroy, inject, DOCUMENT, HostListener, Renderer2, AfterViewInit, + OnInit, } from '@angular/core'; import { IgxOverlayService } from '../../services/overlay/overlay'; -import { OverlaySettings } from '../../services/overlay/utilities'; +import { OverlayInfo, OverlaySettings, OverlaySizeRegistry } from '../../services/overlay/utilities'; import { IgxNavigationService } from '../../core/navigation'; import { IgxToggleDirective } from '../toggle/toggle.directive'; import { IgxTooltipTargetDirective } from './tooltip-target.directive'; @@ -32,7 +33,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 */ @@ -122,6 +123,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( @@ -141,6 +143,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) { @@ -159,6 +167,8 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView if (this.arrow) { this._removeArrow(); } + + this._sizeRegistry.clear(this.element); } /** @@ -234,4 +244,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/src/lib/services/overlay/overlay.spec.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts index 1a553f153b8..749ca97deea 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts @@ -37,6 +37,7 @@ import { OverlayCancelableEventArgs, OverlayEventArgs, OverlaySettings, + OverlaySizeRegistry, Point, PositionSettings, VerticalAlignment @@ -223,6 +224,7 @@ describe('igxOverlay', () => { let mockPlatformUtil: any; let overlay: IgxOverlayService; let mockAnimationService: AnimationService; + let mockOverlaySizeRegistry: OverlaySizeRegistry; beforeEach(() => { mockElement = { style: { visibility: '', cursor: '', transitionDuration: '' }, @@ -291,9 +293,10 @@ describe('igxOverlay', () => { mockNgZone = {}; mockPlatformUtil = { isIOS: false }; mockAnimationService = new IgxAngularAnimationService(mockAnimationBuilder); + mockOverlaySizeRegistry = new OverlaySizeRegistry(); overlay = new IgxOverlayService( - mockApplicationRef, mockDocument, mockNgZone, mockPlatformUtil, mockAnimationService); + mockApplicationRef, mockDocument, mockNgZone, mockPlatformUtil, mockAnimationService, mockOverlaySizeRegistry); }); afterEach(() => { overlay.ngOnDestroy(); @@ -613,8 +616,7 @@ describe('igxOverlay', () => { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false, - cacheSize: true + closeOnEscape: false }; spyOn(overlayInstance.contentAppending, 'emit'); @@ -3550,7 +3552,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(); @@ -3569,15 +3571,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(); })); }); @@ -4733,7 +4726,7 @@ export class TwoButtonsComponent {
-

Some Content

+ Some Content
`, styles: [`button { diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index bdf1456a601..93756e2d720 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -40,6 +40,7 @@ import { OverlayEventArgs, OverlayInfo, OverlaySettings, + OverlaySizeRegistry, Point, PositionSettings, RelativePosition, @@ -138,8 +139,7 @@ export class IgxOverlayService implements OnDestroy { scrollStrategy: new NoOpScrollStrategy(), modal: true, closeOnOutsideClick: true, - closeOnEscape: false, - cacheSize: true + closeOnEscape: false }; constructor( @@ -147,7 +147,8 @@ export class IgxOverlayService implements OnDestroy { @Inject(DOCUMENT) private document: any, private _zone: NgZone, protected platformUtil: PlatformUtil, - @Inject(IgxAngularAnimationService) private animationService: AnimationService) { + @Inject(IgxAngularAnimationService) private animationService: AnimationService, + private sizeRegistry: OverlaySizeRegistry) { this._document = this.document; } @@ -346,17 +347,12 @@ export class IgxOverlayService implements OnDestroy { 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); @@ -1021,4 +1017,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/src/lib/services/overlay/utilities.ts b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts index 18d4ab968c8..aade3be47ec 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts @@ -1,11 +1,41 @@ import { AnimationReferenceMetadata } from '@angular/animations'; -import { ComponentRef, ElementRef, Injector, NgZone } from '@angular/core'; +import { ComponentRef, ElementRef, Injectable, Injector, NgZone } from '@angular/core'; import { CancelableBrowserEventArgs, CancelableEventArgs, cloneValue, IBaseEventArgs } from '../../core/utils'; import { IgxOverlayOutletDirective } from '../../directives/toggle/toggle.directive'; 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); + } +} + /* blazorAlternateName: GridHorizontalAlignment */ export enum HorizontalAlignment { Left = -1, @@ -113,12 +143,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/src/lib/snackbar/snackbar.component.spec.ts b/projects/igniteui-angular/src/lib/snackbar/snackbar.component.spec.ts index a849053337c..d1fec3b5585 100644 --- a/projects/igniteui-angular/src/lib/snackbar/snackbar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/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/src/lib/snackbar/snackbar.component.ts b/projects/igniteui-angular/src/lib/snackbar/snackbar.component.ts index b0a1ef7124a..a9cdd936138 100644 --- a/projects/igniteui-angular/src/lib/snackbar/snackbar.component.ts +++ b/projects/igniteui-angular/src/lib/snackbar/snackbar.component.ts @@ -1,9 +1,12 @@ import { useAnimation } from '@angular/animations'; import { Component, + DOCUMENT, EventEmitter, HostBinding, + inject, Input, + OnDestroy, OnInit, Output } from '@angular/core'; @@ -14,6 +17,7 @@ import { IgxNotificationsDirective } from '../directives/notification/notificati import { ToggleViewEventArgs } from '../directives/toggle/toggle.directive'; import { IgxButtonDirective } from '../directives/button/button.directive'; import { fadeIn, fadeOut } from 'igniteui-angular/animations'; +import { OverlayInfo, OverlaySizeRegistry } from '../services/overlay/utilities'; let NEXT_ID = 0; /** @@ -38,8 +42,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"`; @@ -198,5 +204,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/src/lib/test-utils/tooltip-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tooltip-components.spec.ts index 909195213df..cdf5760b852 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tooltip-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tooltip-components.spec.ts @@ -2,6 +2,9 @@ import { Component, TemplateRef, ViewChild } from '@angular/core'; import { IgxTooltipDirective } from '../directives/tooltip/tooltip.directive'; import { ITooltipHideEventArgs, ITooltipShowEventArgs, IgxTooltipTargetDirective } from '../directives/tooltip/tooltip-target.directive'; import { IgxToggleActionDirective, IgxToggleDirective } from '../directives/toggle/toggle.directive'; +import { IgxIconComponent } from '../icon/icon.component'; +import { IgxIconButtonDirective } from '../directives/button/icon-button.directive'; +import { IgxButtonDirective } from '../directives/button/button.directive'; @Component({ template: ` @@ -204,3 +207,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 806acf8aa1b5b6457d15f185a41be99a9509bca7 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Mon, 16 Feb 2026 10:02:23 +0200 Subject: [PATCH 3/3] chore(*): remove unused el rect --- projects/igniteui-angular/src/lib/services/overlay/overlay.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index 93756e2d720..f17e8f23afc 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -346,7 +346,6 @@ 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 size before moving the container into the overlay so that it does not forget about inherited styles. this.getComponentSize(info); this.setInitialSize(