Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions projects/igniteui-angular/core/src/services/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +44,7 @@ export class IgxOverlayService implements OnDestroy {
private _zone = inject(NgZone);
protected platformUtil = inject(PlatformUtil);
private animationService = inject<AnimationService>(IgxAngularAnimationService);
private sizeRegistry = inject(OverlaySizeRegistry);

/**
* Emitted just before the overlay content starts to open.
Expand Down Expand Up @@ -331,11 +332,12 @@ 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 };
// 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);
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);
Expand Down Expand Up @@ -1000,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 **override** this method 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 {
Comment on lines +1006 to +1018
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc/comment claims consumers can “override this method”, but setInitialSize is private, so it can’t be overridden. The actual extension mechanism is OverlaySizeRegistry.register(...). Reword the comment to reflect the registry-based override (e.g., “Components can register an override via OverlaySizeRegistry to measure after moving”).

Copilot uses AI. Check for mistakes.
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();
}
}
28 changes: 27 additions & 1 deletion projects/igniteui-angular/core/src/services/overlay/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
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';

/** @hidden @internal */
export type SetInitialSizeFn = (info: OverlayInfo, moveToOverlay: () => void) => void;

/** @hidden @internal */
@Injectable({ providedIn: 'root' })
export class OverlaySizeRegistry {
private readonly map = new Map<HTMLElement, SetInitialSizeFn>();

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:
Expand Down
2 changes: 1 addition & 1 deletion projects/igniteui-angular/core/src/services/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OverlaySizeRegistry is introduced in utilities.ts with @hidden @internal JSDoc, but it’s now re-exported from the core public API. This is inconsistent and may unintentionally expose an internal hook to library consumers. Either (1) keep it public and remove/adjust the @internal marker + add minimal API docs, or (2) avoid re-exporting it from the public entrypoint and instead consume it via an internal/shared entrypoint intended for cross-package internals.

Suggested change
IgxOverlayOutletDirective, OverlayInfo, OverlaySizeRegistry
IgxOverlayOutletDirective, OverlayInfo

Copilot uses AI. Check for mistakes.
} from './overlay/utilities';
export * from './transaction/base-transaction';
export * from './transaction/hierarchical-transaction';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
Directive, OnInit, OnDestroy, Output, ElementRef, ViewContainerRef,
Directive, OnInit, OnDestroy, Output, ViewContainerRef,
Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2,
EnvironmentInjector,
createComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, IgxTooltipNestedTooltipsComponent } from '../../../../test-utils/tooltip-components.spec';
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, 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';
Expand Down Expand Up @@ -32,7 +32,8 @@ describe('IgxTooltip', () => {
IgxTooltipWithToggleActionComponent,
IgxTooltipWithCloseButtonComponent,
IgxTooltipWithNestedContentComponent,
IgxTooltipNestedTooltipsComponent
IgxTooltipNestedTooltipsComponent,
IgxTooltipSizeComponent
]
}).compileComponents();
UIInteractions.clearOverlay();
Expand Down Expand Up @@ -980,6 +981,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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
OnDestroy, inject, 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';

Expand All @@ -28,7 +29,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
*/
Expand Down Expand Up @@ -116,6 +117,7 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView
private _role: 'tooltip' | 'status' = 'tooltip';
private _renderer = inject(Renderer2);
private _platformUtil = inject(PlatformUtil);
private _sizeRegistry = inject(OverlaySizeRegistry);

/** @hidden */
public ngAfterViewInit(): void {
Expand All @@ -124,13 +126,21 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView
}
}

/** @hidden */
public override ngOnInit() {
super.ngOnInit();
this._sizeRegistry.register(this.element, this.setInitialSize);
}

/** @hidden */
public override ngOnDestroy() {
super.ngOnDestroy();

if (this.arrow) {
this._removeArrow();
}

this._sizeRegistry.clear(this.element);
}

/**
Expand Down Expand Up @@ -206,4 +216,14 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView
this._arrowEl.remove();
this._arrowEl = null;
}

/**
* 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 };
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ describe('IgxSnackbar', () => {
imports: [
NoopAnimationsModule,
SnackbarInitializeTestComponent,
SnackbarCustomContentComponent
SnackbarCustomContentComponent,
SnackbarSizeTestComponent
]
}).compileComponents();
}));
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -273,3 +296,17 @@ class SnackbarCustomContentComponent {
@ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent;
public text: string;
}

@Component({
template: `
<div style="width: 600px;">
<igx-snackbar #snackbar>
<div style="width: 200px;">Snackbar Message</div>
</igx-snackbar>
</div>
`,
imports: [IgxSnackbarComponent]
})
class SnackbarSizeTestComponent {
@ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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';

Expand All @@ -36,8 +39,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"`;
Expand Down Expand Up @@ -196,5 +201,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 };
};
}
Loading
Loading