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 420cbbfee24..1e76f2e4fdb 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/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 65fbdebe537..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(); diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index e8e511fb15c..f17e8f23afc 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, @@ -146,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; } @@ -344,11 +346,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); @@ -1013,4 +1016,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 3286909a8f2..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, 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'; +}