Skip to content

Commit 6e78973

Browse files
committed
fix(tooltip/snackbar): use overlay service size helpers
1 parent c0a0ed3 commit 6e78973

10 files changed

Lines changed: 176 additions & 45 deletions

File tree

projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,7 @@ describe('igxOverlay', () => {
569569
scrollStrategy: new NoOpScrollStrategy(),
570570
modal: true,
571571
closeOnOutsideClick: true,
572-
closeOnEscape: false,
573-
cacheSize: true
572+
closeOnEscape: false
574573
};
575574

576575
spyOn(overlayInstance.contentAppending, 'emit');
@@ -3507,7 +3506,7 @@ describe('igxOverlay', () => {
35073506
}));
35083507

35093508
// 4. Css
3510-
it('Should use component initial container\'s properties based on cacheSize when it\'s with 100% width and shown in overlay element',
3509+
it('Should use component initial container\'s properties when is with 100% width and show in overlay element',
35113510
fakeAsync(() => {
35123511
const fixture = TestBed.createComponent(WidthTestOverlayComponent);
35133512
fixture.detectChanges();
@@ -3526,16 +3525,6 @@ describe('igxOverlay', () => {
35263525
// content element has no height, so the shown element will calculate its own height by itself
35273526
// expect(overlayChild.style.height).toEqual('100%');
35283527
// expect(overlayChild.getBoundingClientRect().height).toEqual(280);
3529-
3530-
fixture.componentInstance.overlaySettings.cacheSize = false;
3531-
fixture.componentInstance.buttonElement.nativeElement.click();
3532-
tick();
3533-
const componentElement2 = fixture.componentInstance.customComponent.nativeElement;
3534-
expect(componentElement2.style.width).toEqual('100%');
3535-
expect(componentElement2.getBoundingClientRect().width).toEqual(123);
3536-
// Check overlay content element width
3537-
expect(componentElement2.parentElement.getBoundingClientRect().width).toEqual(123);
3538-
fixture.componentInstance.overlay.detachAll();
35393528
}));
35403529
});
35413530

projects/igniteui-angular/core/src/services/overlay/overlay.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ import {
4040
@Injectable({ providedIn: 'root' })
4141
export class IgxOverlayService implements OnDestroy {
4242
private _appRef = inject(ApplicationRef);
43-
private document = inject(DOCUMENT);
4443
private _zone = inject(NgZone);
45-
protected platformUtil = inject(PlatformUtil);
4644
private animationService = inject<AnimationService>(IgxAngularAnimationService);
4745

46+
protected document = inject(DOCUMENT);
47+
protected platformUtil = inject(PlatformUtil);
48+
4849
/**
4950
* Emitted just before the overlay content starts to open.
5051
* ```typescript
@@ -130,8 +131,7 @@ export class IgxOverlayService implements OnDestroy {
130131
scrollStrategy: new NoOpScrollStrategy(),
131132
modal: true,
132133
closeOnOutsideClick: true,
133-
closeOnEscape: false,
134-
cacheSize: true
134+
closeOnEscape: false
135135
};
136136

137137
constructor() {
@@ -332,18 +332,12 @@ export class IgxOverlayService implements OnDestroy {
332332
info.settings = eventArgs.settings;
333333
this._overlayInfos.push(info);
334334
info.hook = this.placeElementHook(info.elementRef.nativeElement);
335-
let elementRect: DOMRect;
336-
// Get the element rect size before moving it into the overlay to cache its size.
337-
if (info.settings.cacheSize) {
338-
elementRect = info.elementRef.nativeElement.getBoundingClientRect();
339-
}
340335
// Get the size before moving the container into the overlay so that it does not forget about inherited styles.
341336
this.getComponentSize(info);
342-
this.moveElementToOverlay(info);
343-
if (!info.settings.cacheSize) {
344-
elementRect = info.elementRef.nativeElement.getBoundingClientRect();
345-
}
346-
info.initialSize = { width: elementRect.width, height: elementRect.height };
337+
this.setInitialSize(
338+
info,
339+
() => this.moveElementToOverlay(info)
340+
);
347341
// Update the container size after moving if there is size.
348342
if (info.size) {
349343
info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size);
@@ -564,6 +558,24 @@ export class IgxOverlayService implements OnDestroy {
564558
return info;
565559
}
566560

561+
/**
562+
* Measures the element's initial size and controls *when* the element is moved into the overlay outlet.
563+
*
564+
* The elements inherit constraining parent styles, so
565+
* for some of them (e.g., Tooltip, Snackbar) their pre-move size is incorrect.
566+
* Those can **override** this method to measure **after** moving to get an accurate size.
567+
*
568+
* - **Default**: Measures in-place (current parent), then moves to the overlay.
569+
*
570+
* @param info OverlayInfo for the content being attached.
571+
* @param moveToOverlay Moves the element into the overlay.
572+
*/
573+
protected setInitialSize(info: OverlayInfo, moveToOverlay: () => void): void {
574+
const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
575+
info.initialSize = { width: elementRect.width, height: elementRect.height };
576+
moveToOverlay();
577+
}
578+
567579
private _hide(id: string, event?: Event) {
568580
const info: OverlayInfo = this.getOverlayById(id);
569581
if (!info) {

projects/igniteui-angular/core/src/services/overlay/utilities.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,6 @@ export interface OverlaySettings {
134134
* Clicking on the elements in this collection will not close the overlay when closeOnOutsideClick = true.
135135
*/
136136
excludeFromOutsideClick?: HTMLElement[];
137-
/**
138-
* @hidden @internal
139-
* Controls whether element size is measured before (true) or after (false) moving to the overlay container.
140-
* Default is true to retain element size.
141-
*/
142-
cacheSize?: boolean;
143137
}
144138

145139
export interface OverlayEventArgs extends IBaseEventArgs {

projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective
8686
closeOnEscape: false,
8787
closeOnOutsideClick: false,
8888
modal: false,
89-
outlet: this.outlet,
90-
cacheSize: false
89+
outlet: this.outlet
9190
};
9291

9392
super.open(overlaySettings);

projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
Directive, OnInit, OnDestroy, Output, ElementRef, ViewContainerRef,
2+
Directive, OnInit, OnDestroy, Output, ViewContainerRef,
33
Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2,
44
EnvironmentInjector,
55
createComponent,
@@ -425,7 +425,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
425425
this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionSettings);
426426
this._overlayDefaults.closeOnOutsideClick = false;
427427
this._overlayDefaults.closeOnEscape = true;
428-
this._overlayDefaults.cacheSize = false;
429428

430429
this.target.closing.pipe(takeUntil(this._destroy$)).subscribe((event) => {
431430
if (this.target.tooltipTarget !== this) {

projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { DebugElement } from '@angular/core';
22
import { fakeAsync, TestBed, tick, flush, waitForAsync, ComponentFixture } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5-
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, IgxTooltipNestedTooltipsComponent } from '../../../../test-utils/tooltip-components.spec';
5+
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, IgxTooltipWithCloseButtonComponent, IgxTooltipWithNestedContentComponent, IgxTooltipNestedTooltipsComponent, IgxTooltipSizeComponent } from '../../../../test-utils/tooltip-components.spec';
66
import { UIInteractions } from '../../../../test-utils/ui-interactions.spec';
77
import { HorizontalAlignment, VerticalAlignment, AutoPositionStrategy } from '../../../../core/src/services/public_api';
88
import { IgxTooltipDirective } from './tooltip.directive';
@@ -32,7 +32,8 @@ describe('IgxTooltip', () => {
3232
IgxTooltipWithToggleActionComponent,
3333
IgxTooltipWithCloseButtonComponent,
3434
IgxTooltipWithNestedContentComponent,
35-
IgxTooltipNestedTooltipsComponent
35+
IgxTooltipNestedTooltipsComponent,
36+
IgxTooltipSizeComponent
3637
]
3738
}).compileComponents();
3839
UIInteractions.clearOverlay();
@@ -980,6 +981,31 @@ describe('IgxTooltip', () => {
980981

981982
expect(fix.componentInstance.toggleDir.collapsed).toBe(false);
982983
}));
984+
985+
it('correctly sizes the tooltip/overlay content when inside an element - issue #16458', fakeAsync(() => {
986+
const fixture = TestBed.createComponent(IgxTooltipSizeComponent);
987+
fixture.detectChanges();
988+
989+
fixture.componentInstance.target1.showTooltip();
990+
fixture.componentInstance.target2.showTooltip();
991+
fixture.componentInstance.target3.showTooltip();
992+
flush();
993+
994+
const tooltip1Rect = fixture.componentInstance.tooltip1.element.getBoundingClientRect();
995+
const tooltip2Rect = fixture.componentInstance.tooltip2.element.getBoundingClientRect();
996+
const tooltip3Rect = fixture.componentInstance.tooltip3.element.getBoundingClientRect();
997+
998+
const tooltip1ParentRect = fixture.componentInstance.tooltip1.element.parentElement.getBoundingClientRect();
999+
const tooltip2ParentRect = fixture.componentInstance.tooltip2.element.parentElement.getBoundingClientRect();
1000+
const tooltip3ParentRect = fixture.componentInstance.tooltip3.element.parentElement.getBoundingClientRect();
1001+
1002+
expect(tooltip1Rect.width).toEqual(tooltip1ParentRect.width);
1003+
expect(tooltip1Rect.height).toEqual(tooltip1ParentRect.height);
1004+
expect(tooltip2Rect.width).toEqual(tooltip2ParentRect.width);
1005+
expect(tooltip2Rect.height).toEqual(tooltip2ParentRect.height);
1006+
expect(tooltip3Rect.width).toEqual(tooltip3ParentRect.width);
1007+
expect(tooltip3Rect.height).toEqual(tooltip3ParentRect.height);
1008+
}));
9831009
});
9841010

9851011
describe('Tooltip Sticky with Close Button', () => {

projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@ import {
33
OnDestroy, inject, HostListener,
44
Renderer2,
55
AfterViewInit,
6+
Injectable,
67
} from '@angular/core';
7-
import { OverlaySettings, PlatformUtil } from 'igniteui-angular/core';
8+
import { IgxOverlayService, OverlaySettings, PlatformUtil } from 'igniteui-angular/core';
89
import { IgxToggleDirective } from '../toggle/toggle.directive';
910
import { IgxTooltipTargetDirective } from './tooltip-target.directive';
11+
import { OverlayInfo } from '../../../../core/src/services/overlay/utilities';
12+
13+
/**
14+
* Measures **after** moving the element into the overlay outlet so that parent
15+
* style constraints do not affect the initial size.
16+
*/
17+
@Injectable()
18+
export class TooltipOverlayServiceHelper extends IgxOverlayService {
19+
protected override setInitialSize(info: OverlayInfo, moveToOverlay: () => void): void {
20+
moveToOverlay();
21+
const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
22+
info.initialSize = { width: elementRect.width, height: elementRect.height };
23+
}
24+
}
1025

1126
let NEXT_ID = 0;
1227
/**
@@ -26,7 +41,13 @@ let NEXT_ID = 0;
2641
@Directive({
2742
exportAs: 'tooltip',
2843
selector: '[igxTooltip]',
29-
standalone: true
44+
standalone: true,
45+
providers: [
46+
{
47+
provide: IgxOverlayService,
48+
useClass: TooltipOverlayServiceHelper
49+
}
50+
]
3051
})
3152
export class IgxTooltipDirective extends IgxToggleDirective implements AfterViewInit, OnDestroy {
3253
/**

projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ describe('IgxSnackbar', () => {
1414
imports: [
1515
NoopAnimationsModule,
1616
SnackbarInitializeTestComponent,
17-
SnackbarCustomContentComponent
17+
SnackbarCustomContentComponent,
18+
SnackbarSizeTestComponent
1819
]
1920
}).compileComponents();
2021
}));
@@ -183,6 +184,27 @@ describe('IgxSnackbar', () => {
183184
expect(customPositionSettings.openAnimation.options.params).toEqual({duration: '1000ms'});
184185
expect(customPositionSettings.minSize).toEqual({height: 100, width: 100});
185186
});
187+
188+
it('correctly sizes the snackbar/overlay content when inside an element - issue #16458', () => {
189+
const fix = TestBed.createComponent(SnackbarSizeTestComponent);
190+
fix.detectChanges();
191+
snackbar = fix.componentInstance.snackbar;
192+
193+
const parentDivRect = snackbar.element.parentElement.getBoundingClientRect();
194+
expect(parentDivRect.width).toBe(600);
195+
196+
snackbar.open();
197+
fix.detectChanges();
198+
199+
const snackbarRect = snackbar.element.getBoundingClientRect();
200+
const overlayContentRect = snackbar.element.parentElement.getBoundingClientRect();
201+
const { marginLeft, marginRight, paddingLeft, paddingRight } = getComputedStyle(snackbar.element);
202+
const horizontalMargins = parseFloat(marginLeft) + parseFloat(marginRight);
203+
const horizontalPaddings = parseFloat(paddingLeft) + parseFloat(paddingRight);
204+
205+
expect(snackbarRect.width).toEqual(200 + horizontalPaddings);
206+
expect(overlayContentRect.width).toEqual(snackbarRect.width + horizontalMargins);
207+
});
186208
});
187209

188210
describe('IgxSnackbar with custom content', () => {
@@ -273,3 +295,17 @@ class SnackbarCustomContentComponent {
273295
@ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent;
274296
public text: string;
275297
}
298+
299+
@Component({
300+
template: `
301+
<div style="width: 600px;">
302+
<igx-snackbar #snackbar>
303+
<div style="width: 200px;">Snackbar Message</div>
304+
</igx-snackbar>
305+
</div>
306+
`,
307+
imports: [IgxSnackbarComponent]
308+
})
309+
class SnackbarSizeTestComponent {
310+
@ViewChild(IgxSnackbarComponent, { static: true }) public snackbar: IgxSnackbarComponent;
311+
}

projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,34 @@ import {
33
Component,
44
EventEmitter,
55
HostBinding,
6+
Injectable,
67
Input,
78
OnInit,
89
Output
910
} from '@angular/core';
1011
import { takeUntil } from 'rxjs/operators';
1112
import { ContainerPositionStrategy, GlobalPositionStrategy, HorizontalAlignment,
12-
PositionSettings, VerticalAlignment } from 'igniteui-angular/core';
13+
IgxOverlayService, PositionSettings, VerticalAlignment } from 'igniteui-angular/core';
1314
import { ToggleViewEventArgs, IgxButtonDirective, IgxNotificationsDirective } from 'igniteui-angular/directives';
1415
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
16+
import { OverlayInfo } from '../../../core/src/services/overlay/utilities';
17+
18+
/**
19+
* Measures **after** moving the element into the overlay outlet so that parent
20+
* style constraints do not affect the initial size.
21+
*/
22+
@Injectable()
23+
export class SnackbarOverlayServiceHelper extends IgxOverlayService {
24+
protected override setInitialSize(info: OverlayInfo, moveToOverlay: () => void): void {
25+
moveToOverlay();
26+
const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
27+
// Needs full element width (margins included) to set proper width for the overlay container.
28+
// Otherwise, the snackbar appears smaller and the text inside it might be misaligned.
29+
const styles = this.document.defaultView.getComputedStyle(info.elementRef.nativeElement);
30+
const horizontalMargins = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight);
31+
info.initialSize = { width: elementRect.width + horizontalMargins, height: elementRect.height };
32+
}
33+
}
1534

1635
let NEXT_ID = 0;
1736
/**
@@ -34,7 +53,13 @@ let NEXT_ID = 0;
3453
@Component({
3554
selector: 'igx-snackbar',
3655
templateUrl: 'snackbar.component.html',
37-
imports: [IgxButtonDirective]
56+
imports: [IgxButtonDirective],
57+
providers: [
58+
{
59+
provide: IgxOverlayService,
60+
useClass: SnackbarOverlayServiceHelper
61+
}
62+
],
3863
})
3964
export class IgxSnackbarComponent extends IgxNotificationsDirective
4065
implements OnInit {

projects/igniteui-angular/test-utils/tooltip-components.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, TemplateRef, ViewChild } from '@angular/core';
2-
import { IgxToggleActionDirective, IgxToggleDirective, IgxTooltipDirective, IgxTooltipTargetDirective, ITooltipHideEventArgs, ITooltipShowEventArgs } from 'igniteui-angular/directives';
2+
import { IgxButtonDirective, IgxIconButtonDirective, IgxToggleActionDirective, IgxToggleDirective, IgxTooltipDirective, IgxTooltipTargetDirective, ITooltipHideEventArgs, ITooltipShowEventArgs } from 'igniteui-angular/directives';
3+
import { IgxIconComponent } from 'igniteui-angular/icon';
34

45

56
@Component({
@@ -207,3 +208,32 @@ export class IgxTooltipNestedTooltipsComponent {
207208
@ViewChild('tooltipLevel2', { read: IgxTooltipDirective, static: true }) public tooltipLevel2: IgxTooltipDirective;
208209
@ViewChild('tooltipLevel3', { read: IgxTooltipDirective, static: true }) public tooltipLevel3: IgxTooltipDirective;
209210
}
211+
212+
@Component({
213+
template: `
214+
<div #target1 style="background: red; width: 20px; height: 20px;" [igxTooltipTarget]="tooltip1">
215+
<div #tooltip1="tooltip" igxTooltip>{{ message }}</div>
216+
</div>
217+
<button #target2 igxIconButton="flat" [igxTooltipTarget]="tooltip2">
218+
<igx-icon>palette</igx-icon>
219+
<div #tooltip2="tooltip" igxTooltip>{{ message }}</div>
220+
</button>
221+
<button #target3 igxButton="flat" [igxTooltipTarget]="tooltip3">
222+
Button
223+
<div #tooltip3="tooltip" igxTooltip>{{ message }}</div>
224+
</button>
225+
`,
226+
imports: [IgxTooltipDirective, IgxTooltipTargetDirective, IgxIconComponent, IgxIconButtonDirective, IgxButtonDirective],
227+
standalone: true
228+
})
229+
export class IgxTooltipSizeComponent {
230+
@ViewChild('target1', { read: IgxTooltipTargetDirective, static: true }) public target1: IgxTooltipTargetDirective;
231+
@ViewChild('target2', { read: IgxTooltipTargetDirective, static: true }) public target2: IgxTooltipTargetDirective;
232+
@ViewChild('target3', { read: IgxTooltipTargetDirective, static: true }) public target3: IgxTooltipTargetDirective;
233+
234+
@ViewChild('tooltip1', { read: IgxTooltipDirective, static: true }) public tooltip1: IgxTooltipDirective;
235+
@ViewChild('tooltip2', { read: IgxTooltipDirective, static: true }) public tooltip2: IgxTooltipDirective;
236+
@ViewChild('tooltip3', { read: IgxTooltipDirective, static: true }) public tooltip3: IgxTooltipDirective;
237+
238+
public message: string = 'Long tooltip message for testing purposes';
239+
}

0 commit comments

Comments
 (0)