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 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();
}
}
32 changes: 31 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,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<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
} 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
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, 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';
Expand Down Expand Up @@ -30,7 +30,8 @@ describe('IgxTooltip', () => {
IgxTooltipWithToggleActionComponent,
IgxTooltipWithCloseButtonComponent,
IgxTooltipWithNestedContentComponent,
IgxTooltipNestedTooltipsComponent
IgxTooltipNestedTooltipsComponent,
IgxTooltipSizeComponent
]
}).compileComponents();
UIInteractions.clearOverlay();
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
*/
Expand Down Expand Up @@ -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() {
Expand All @@ -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) {
Expand All @@ -151,6 +159,8 @@ export class IgxTooltipDirective extends IgxToggleDirective implements AfterView
if (this.arrow) {
this._removeArrow();
}

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

/**
Expand Down Expand Up @@ -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 };
}
}
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,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';

Expand All @@ -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"`;
Expand Down Expand Up @@ -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 };
}
}
Loading
Loading