• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.13
/projects/igniteui-angular/src/lib/services/overlay/overlay.ts
1
import { AnimationReferenceMetadata } from '@angular/animations';
2
import { DOCUMENT } from '@angular/common';
3
import {
4
    ApplicationRef,
5
    ComponentRef,
6
    createComponent,
7
    ElementRef,
8
    EventEmitter,
9
    Inject,
10
    Injectable,
11
    Injector,
12
    NgZone,
13
    OnDestroy,
14
    Type,
15
    ViewContainerRef
16
} from '@angular/core';
17
import { fromEvent, Subject, Subscription } from 'rxjs';
18
import { filter, takeUntil } from 'rxjs/operators';
19

20
import { fadeIn, fadeOut, IAnimationParams, scaleInHorLeft, scaleInHorRight, scaleInVerBottom, scaleInVerTop, scaleOutHorLeft, scaleOutHorRight, scaleOutVerBottom, scaleOutVerTop, slideInBottom, slideInTop, slideOutBottom, slideOutTop } from 'igniteui-angular/animations';
21
import { PlatformUtil } from '../../core/utils';
22
import { IgxOverlayOutletDirective } from '../../directives/toggle/toggle.directive';
23
import { IgxAngularAnimationService } from '../animation/angular-animation-service';
24
import { AnimationService } from '../animation/animation';
25
import { AutoPositionStrategy } from './position/auto-position-strategy';
26
import { ConnectedPositioningStrategy } from './position/connected-positioning-strategy';
27
import { ContainerPositionStrategy } from './position/container-position-strategy';
28
import { ElasticPositionStrategy } from './position/elastic-position-strategy';
29
import { GlobalPositionStrategy } from './position/global-position-strategy';
30
import { IPositionStrategy } from './position/IPositionStrategy';
31
import { NoOpScrollStrategy } from './scroll/NoOpScrollStrategy';
32
import {
33
    AbsolutePosition,
34
    HorizontalAlignment,
35
    OffsetMode,
36
    OverlayAnimationEventArgs,
37
    OverlayCancelableEventArgs,
38
    OverlayClosingEventArgs,
39
    OverlayCreateSettings,
40
    OverlayEventArgs,
41
    OverlayInfo,
42
    OverlaySettings,
43
    Point,
44
    PositionSettings,
45
    RelativePosition,
46
    RelativePositionStrategy,
47
    VerticalAlignment
48
} from './utilities';
49

50
/**
51
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/overlay-main)
52
 * The overlay service allows users to show components on overlay div above all other elements in the page.
53
 */
54
@Injectable({ providedIn: 'root' })
55
export class IgxOverlayService implements OnDestroy {
2✔
56
    /**
57
     * Emitted just before the overlay content starts to open.
58
     * ```typescript
59
     * opening(event: OverlayCancelableEventArgs){
60
     *     const opening = event;
61
     * }
62
     * ```
63
     */
64
    public opening = new EventEmitter<OverlayCancelableEventArgs>();
35✔
65

66
    /**
67
     * Emitted after the overlay content is opened and all animations are finished.
68
     * ```typescript
69
     * opened(event: OverlayEventArgs){
70
     *     const opened = event;
71
     * }
72
     * ```
73
     */
74
    public opened = new EventEmitter<OverlayEventArgs>();
35✔
75

76
    /**
77
     * Emitted just before the overlay content starts to close.
78
     * ```typescript
79
     * closing(event: OverlayCancelableEventArgs){
80
     *     const closing = event;
81
     * }
82
     * ```
83
     */
84
    public closing = new EventEmitter<OverlayClosingEventArgs>();
35✔
85

86
    /**
87
     * Emitted after the overlay content is closed and all animations are finished.
88
     * ```typescript
89
     * closed(event: OverlayEventArgs){
90
     *     const closed = event;
91
     * }
92
     * ```
93
     */
94
    public closed = new EventEmitter<OverlayEventArgs>();
35✔
95

96
    /**
97
     * Emitted before the content is appended to the overlay.
98
     * ```typescript
99
     * contentAppending(event: OverlayEventArgs){
100
     *     const contentAppending = event;
101
     * }
102
     * ```
103
     */
104
    public contentAppending = new EventEmitter<OverlayEventArgs>();
35✔
105

106
    /**
107
     * Emitted after the content is appended to the overlay, and before animations are started.
108
     * ```typescript
109
     * contentAppended(event: OverlayEventArgs){
110
     *     const contentAppended = event;
111
     * }
112
     * ```
113
     */
114
    public contentAppended = new EventEmitter<OverlayEventArgs>();
35✔
115

116
    /**
117
     * Emitted just before the overlay animation start.
118
     * ```typescript
119
     * animationStarting(event: OverlayAnimationEventArgs){
120
     *     const animationStarting = event;
121
     * }
122
     * ```
123
     */
124
    public animationStarting = new EventEmitter<OverlayAnimationEventArgs>();
35✔
125

126
    private _componentId = 0;
35✔
127
    private _overlayInfos: OverlayInfo[] = [];
35✔
128
    private _overlayElement: HTMLElement;
129
    private _document: Document;
130
    private _keyPressEventListener: Subscription;
131
    private destroy$ = new Subject<boolean>();
35✔
132
    private _cursorStyleIsSet = false;
35✔
133
    private _cursorOriginalValue: string;
134

135
    private _defaultSettings: OverlaySettings = {
35✔
136
        excludeFromOutsideClick: [],
137
        positionStrategy: new GlobalPositionStrategy(),
138
        scrollStrategy: new NoOpScrollStrategy(),
139
        modal: true,
140
        closeOnOutsideClick: true,
141
        closeOnEscape: false
142
    };
143

144
    constructor(
145
        private _appRef: ApplicationRef,
35✔
146
        @Inject(DOCUMENT) private document: any,
35✔
147
        private _zone: NgZone,
35✔
148
        protected platformUtil: PlatformUtil,
35✔
149
        @Inject(IgxAngularAnimationService) private animationService: AnimationService) {
35✔
150
        this._document = this.document;
35✔
151
    }
152

153
    /**
154
     * Creates overlay settings with global or container position strategy and preset position settings
155
     *
156
     * @param position Preset position settings. Default position is 'center'
157
     * @param outlet The outlet container to attach the overlay to
158
     * @returns Non-modal overlay settings based on Global or Container position strategy and the provided position.
159
     */
160
    public static createAbsoluteOverlaySettings(
161
        position?: AbsolutePosition, outlet?: IgxOverlayOutletDirective | ElementRef): OverlaySettings {
162
        const positionSettings = this.createAbsolutePositionSettings(position);
×
163
        const strategy = outlet ? new ContainerPositionStrategy(positionSettings) : new GlobalPositionStrategy(positionSettings);
×
164
        const overlaySettings: OverlaySettings = {
×
165
            positionStrategy: strategy,
166
            scrollStrategy: new NoOpScrollStrategy(),
167
            modal: false,
168
            closeOnOutsideClick: true,
169
            outlet
170
        };
171
        return overlaySettings;
×
172
    }
173

174
    /**
175
     * Creates overlay settings with auto, connected or elastic position strategy and preset position settings
176
     *
177
     * @param target Attaching target for the component to show
178
     * @param strategy The relative position strategy to be applied to the overlay settings. Default is Auto positioning strategy.
179
     * @param position Preset position settings. By default the element is positioned below the target, left aligned.
180
     * @returns Non-modal overlay settings based on the provided target, strategy and position.
181
     */
182
    public static createRelativeOverlaySettings(
183
        target: Point | HTMLElement,
184
        position?: RelativePosition,
185
        strategy?: RelativePositionStrategy):
186
        OverlaySettings {
187
        const positionSettings = this.createRelativePositionSettings(position);
×
188
        const overlaySettings: OverlaySettings = {
×
189
            target,
190
            positionStrategy: this.createPositionStrategy(strategy, positionSettings),
191
            scrollStrategy: new NoOpScrollStrategy(),
192
            modal: false,
193
            closeOnOutsideClick: true
194
        };
195
        return overlaySettings;
×
196
    }
197

198
    private static createAbsolutePositionSettings(position: AbsolutePosition): PositionSettings {
199
        let positionSettings: PositionSettings;
200
        switch (position) {
×
201
            case AbsolutePosition.Bottom:
202
                positionSettings = {
×
203
                    horizontalDirection: HorizontalAlignment.Center,
204
                    verticalDirection: VerticalAlignment.Bottom,
205
                    openAnimation: slideInBottom,
206
                    closeAnimation: slideOutBottom
207
                };
208
                break;
×
209
            case AbsolutePosition.Top:
210
                positionSettings = {
×
211
                    horizontalDirection: HorizontalAlignment.Center,
212
                    verticalDirection: VerticalAlignment.Top,
213
                    openAnimation: slideInTop,
214
                    closeAnimation: slideOutTop
215
                };
216
                break;
×
217
            case AbsolutePosition.Center:
218
            default:
219
                positionSettings = {
×
220
                    horizontalDirection: HorizontalAlignment.Center,
221
                    verticalDirection: VerticalAlignment.Middle,
222
                    openAnimation: fadeIn,
223
                    closeAnimation: fadeOut
224
                };
225
        }
226
        return positionSettings;
×
227
    }
228

229
    private static createRelativePositionSettings(position: RelativePosition): PositionSettings {
230
        let positionSettings: PositionSettings;
231
        switch (position) {
×
232
            case RelativePosition.Above:
233
                positionSettings = {
×
234
                    horizontalStartPoint: HorizontalAlignment.Center,
235
                    verticalStartPoint: VerticalAlignment.Top,
236
                    horizontalDirection: HorizontalAlignment.Center,
237
                    verticalDirection: VerticalAlignment.Top,
238
                    openAnimation: scaleInVerBottom,
239
                    closeAnimation: scaleOutVerBottom,
240
                };
241
                break;
×
242
            case RelativePosition.Below:
243
                positionSettings = {
×
244
                    horizontalStartPoint: HorizontalAlignment.Center,
245
                    verticalStartPoint: VerticalAlignment.Bottom,
246
                    horizontalDirection: HorizontalAlignment.Center,
247
                    verticalDirection: VerticalAlignment.Bottom,
248
                    openAnimation: scaleInVerTop,
249
                    closeAnimation: scaleOutVerTop
250
                };
251
                break;
×
252
            case RelativePosition.After:
253
                positionSettings = {
×
254
                    horizontalStartPoint: HorizontalAlignment.Right,
255
                    verticalStartPoint: VerticalAlignment.Middle,
256
                    horizontalDirection: HorizontalAlignment.Right,
257
                    verticalDirection: VerticalAlignment.Middle,
258
                    openAnimation: scaleInHorLeft,
259
                    closeAnimation: scaleOutHorLeft
260
                };
261
                break;
×
262
            case RelativePosition.Before:
263
                positionSettings = {
×
264
                    horizontalStartPoint: HorizontalAlignment.Left,
265
                    verticalStartPoint: VerticalAlignment.Middle,
266
                    horizontalDirection: HorizontalAlignment.Left,
267
                    verticalDirection: VerticalAlignment.Middle,
268
                    openAnimation: scaleInHorRight,
269
                    closeAnimation: scaleOutHorRight
270
                };
271
                break;
×
272
            case RelativePosition.Default:
273
            default:
274
                positionSettings = {
×
275
                    horizontalStartPoint: HorizontalAlignment.Left,
276
                    verticalStartPoint: VerticalAlignment.Bottom,
277
                    horizontalDirection: HorizontalAlignment.Right,
278
                    verticalDirection: VerticalAlignment.Bottom,
279
                    openAnimation: scaleInVerTop,
280
                    closeAnimation: scaleOutVerTop,
281
                };
282
                break;
×
283
        }
284
        return positionSettings;
×
285
    }
286

287
    private static createPositionStrategy(strategy: RelativePositionStrategy, positionSettings: PositionSettings): IPositionStrategy {
288
        switch (strategy) {
×
289
            case RelativePositionStrategy.Connected:
290
                return new ConnectedPositioningStrategy(positionSettings);
×
291
            case RelativePositionStrategy.Elastic:
292
                return new ElasticPositionStrategy(positionSettings);
×
293
            case RelativePositionStrategy.Auto:
294
            default:
295
                return new AutoPositionStrategy(positionSettings);
×
296
        }
297
    }
298

299
    /**
300
     * Generates Id. Provide this Id when call `show(id)` method
301
     *
302
     * @param component ElementRef to show in overlay
303
     * @param settings (optional): Display settings for the overlay, such as positioning and scroll/close behavior.
304
     * @returns Id of the created overlay. Valid until `detach` is called.
305
     */
306
    public attach(element: ElementRef, settings?: OverlaySettings): string;
307
    /**
308
     * Generates Id. Provide this Id when call `show(id)` method
309
     *
310
     * Note created instance is in root scope, prefer the `viewContainerRef` overload when local injection context is needed.
311
     *
312
     * @param component Component Type to show in overlay
313
     * @param settings (optional): Create settings for the overlay, such as positioning and scroll/close behavior.
314
     * Includes also an optional `Injector` to add to the created dynamic component's injectors.
315
     * @returns Id of the created overlay. Valid until `detach` is called.
316
     */
317
    public attach(component: Type<any>, settings?: OverlayCreateSettings): string;
318
    // TODO: change third parameter to OverlayCreateSettings and allow passing of Injector and so on.
319
    /**
320
     * Generates an Id. Provide this Id when calling the `show(id)` method
321
     *
322
     * @param component Component Type to show in overlay
323
     * @param viewContainerRef Reference to the container where created component's host view will be inserted
324
     * @param settings (optional): Display settings for the overlay, such as positioning and scroll/close behavior.
325
     */
326
    public attach(component: Type<any>, viewContainerRef: ViewContainerRef, settings?: OverlaySettings): string;
327
    public attach(
328
        componentOrElement: ElementRef | Type<any>,
329
        viewContainerRefOrSettings?: ViewContainerRef | OverlayCreateSettings,
330
        settings?: OverlaySettings): string {
331
        const info: OverlayInfo = this.getOverlayInfo(componentOrElement, viewContainerRefOrSettings, settings);
78✔
332

333
        if (!info) {
78!
334
            console.warn('Overlay was not able to attach provided component!');
×
335
            return null;
×
336
        }
337

338
        info.id = (this._componentId++).toString();
78✔
339
        info.visible = false;
78✔
340
        // Emit the contentAppending event before appending the content
341
        const eventArgs = { id: info.id, elementRef: info.elementRef, componentRef: info.componentRef, settings: info.settings };
78✔
342
        this.contentAppending.emit(eventArgs);
78✔
343
        // Append the content to the overlay
344
        info.settings = eventArgs.settings;
78✔
345
        this._overlayInfos.push(info);
78✔
346
        info.hook = this.placeElementHook(info.elementRef.nativeElement);
78✔
347
        const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
78✔
348
        info.initialSize = { width: elementRect.width, height: elementRect.height };
78✔
349
        // Get the size before moving the container into the overlay so that it does not forget about inherited styles.
350
        this.getComponentSize(info);
78✔
351
        this.moveElementToOverlay(info);
78✔
352
        // Update the container size after moving if there is size.
353
        if (info.size) {
78✔
354
            info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size);
41✔
355
        }
356
        this.contentAppended.emit({ id: info.id, componentRef: info.componentRef });
78✔
357
        info.settings.scrollStrategy.initialize(this._document, this, info.id);
78✔
358
        info.settings.scrollStrategy.attach();
78✔
359
        this.addOutsideClickListener(info);
78✔
360
        this.addResizeHandler();
78✔
361
        this.addCloseOnEscapeListener(info);
78✔
362
        this.buildAnimationPlayers(info);
78✔
363
        return info.id;
78✔
364
    }
365

366
    /**
367
     * Remove overlay with the provided id.
368
     *
369
     * @param id Id of the overlay to remove
370
     * ```typescript
371
     * this.overlay.detach(id);
372
     * ```
373
     */
374
    public detach(id: string) {
375
        const info: OverlayInfo = this.getOverlayById(id);
78✔
376

377
        if (!info) {
78!
UNCOV
378
            console.warn('igxOverlay.detach was called with wrong id: ', id);
×
UNCOV
379
            return;
×
380
        }
381
        info.detached = true;
78✔
382
        this.finishAnimations(info);
78✔
383
        info.settings.scrollStrategy.detach();
78✔
384
        this.removeOutsideClickListener(info);
78✔
385
        this.removeResizeHandler();
78✔
386
        this.cleanUp(info);
78✔
387
    }
388

389
    /**
390
     * Remove all the overlays.
391
     * ```typescript
392
     * this.overlay.detachAll();
393
     * ```
394
     */
395
    public detachAll() {
UNCOV
396
        for (let i = this._overlayInfos.length; i--;) {
×
UNCOV
397
            this.detach(this._overlayInfos[i].id);
×
398
        }
399
    }
400

401
    /**
402
     * Shows the overlay for provided id.
403
     *
404
     * @param id Id to show overlay for
405
     * @param settings Display settings for the overlay, such as positioning and scroll/close behavior.
406
     */
407
    public show(id: string, settings?: OverlaySettings): void {
408
        const info: OverlayInfo = this.getOverlayById(id);
78✔
409
        if (!info) {
78!
410
            console.warn('igxOverlay.show was called with wrong id: ', id);
×
411
            return;
×
412
        }
413
        const eventArgs: OverlayCancelableEventArgs = { id, componentRef: info.componentRef, cancel: false };
78✔
414
        this.opening.emit(eventArgs);
78✔
415
        if (eventArgs.cancel) {
78!
UNCOV
416
            return;
×
417
        }
418
        if (settings) {
78✔
419
            settings.positionStrategy ??= info.settings.positionStrategy;
41✔
420
            settings.scrollStrategy ??= info.settings.scrollStrategy;
41✔
421
            info.settings = { ...info.settings, ...settings };
41✔
422
        }
423
        this.updateSize(info);
78✔
424
        info.settings.positionStrategy.position(
78✔
425
            info.elementRef.nativeElement.parentElement,
426
            { width: info.initialSize.width, height: info.initialSize.height },
427
            this._document,
428
            true,
429
            info.settings.target);
430
        this.addModalClasses(info);
78✔
431
        if (info.settings.positionStrategy.settings.openAnimation) {
78!
432
            // TODO: should we build players again. This was already done in attach!!!
433
            // this.buildAnimationPlayers(info);
434
            this.playOpenAnimation(info);
78✔
435
        } else {
436
            //  to eliminate flickering show the element just before opened fires
UNCOV
437
            info.wrapperElement.style.visibility = '';
×
UNCOV
438
            info.visible = true;
×
UNCOV
439
            this.opened.emit({ id: info.id, componentRef: info.componentRef });
×
440
        }
441
    }
442

443
    /**
444
     * Hides the component with the ID provided as a parameter.
445
     * ```typescript
446
     * this.overlay.hide(id);
447
     * ```
448
     */
449
    public hide(id: string, event?: Event) {
450
        this._hide(id, event);
57✔
451
    }
452

453
    /**
454
     * Hides all the components and the overlay.
455
     * ```typescript
456
     * this.overlay.hideAll();
457
     * ```
458
     */
459
    public hideAll() {
UNCOV
460
        for (let i = this._overlayInfos.length; i--;) {
×
UNCOV
461
            this.hide(this._overlayInfos[i].id);
×
462
        }
463
    }
464

465
    /**
466
     * Repositions the component with ID provided as a parameter.
467
     *
468
     * @param id Id to reposition overlay for
469
     * ```typescript
470
     * this.overlay.reposition(id);
471
     * ```
472
     */
473
    public reposition(id: string) {
UNCOV
474
        const overlayInfo = this.getOverlayById(id);
×
UNCOV
475
        if (!overlayInfo || !overlayInfo.settings) {
×
UNCOV
476
            console.warn('Wrong id provided in overlay.reposition method. Id: ', id);
×
UNCOV
477
            return;
×
478
        }
UNCOV
479
        if (!overlayInfo.visible) {
×
UNCOV
480
            return;
×
481
        }
UNCOV
482
        const contentElement = overlayInfo.elementRef.nativeElement.parentElement;
×
UNCOV
483
        const contentElementRect = contentElement.getBoundingClientRect();
×
UNCOV
484
        overlayInfo.settings.positionStrategy.position(
×
485
            contentElement,
486
            {
487
                width: contentElementRect.width,
488
                height: contentElementRect.height
489
            },
490
            this._document,
491
            false,
492
            overlayInfo.settings.target);
493
    }
494

495
    /**
496
     * Offsets the content along the corresponding axis by the provided amount with optional offsetMode that determines whether to add (by default) or set the offset values
497
     *
498
     * @param id Id to offset overlay for
499
     * @param deltaX Amount of offset in horizontal direction
500
     * @param deltaY Amount of offset in vertical direction
501
     * @param offsetMode Determines whether to add (by default) or set the offset values with OffsetMode.Add and OffsetMode.Set
502
     * ```typescript
503
     * this.overlay.setOffset(id, deltaX, deltaY, offsetMode);
504
     * ```
505
     */
506
    public setOffset(id: string, deltaX: number, deltaY: number, offsetMode?: OffsetMode) {
UNCOV
507
        const info: OverlayInfo = this.getOverlayById(id);
×
508

UNCOV
509
        if (!info) {
×
UNCOV
510
            return;
×
511
        }
512

UNCOV
513
        switch (offsetMode) {
×
514
            case OffsetMode.Set:
UNCOV
515
                info.transformX = deltaX;
×
UNCOV
516
                info.transformY = deltaY;
×
UNCOV
517
                break;
×
518
            case OffsetMode.Add:
519
            default:
UNCOV
520
                info.transformX += deltaX;
×
UNCOV
521
                info.transformY += deltaY;
×
UNCOV
522
                break;
×
523
        }
524

UNCOV
525
        const transformX = info.transformX;
×
UNCOV
526
        const transformY = info.transformY;
×
527

UNCOV
528
        const translate = `translate(${transformX}px, ${transformY}px)`;
×
UNCOV
529
        info.elementRef.nativeElement.parentElement.style.transform = translate;
×
530
    }
531

532
    /** @hidden */
533
    public repositionAll = () => {
35✔
UNCOV
534
        for (let i = this._overlayInfos.length; i--;) {
×
UNCOV
535
            this.reposition(this._overlayInfos[i].id);
×
536
        }
537
    };
538

539
    /** @hidden */
540
    public ngOnDestroy(): void {
UNCOV
541
        this.destroy$.next(true);
×
UNCOV
542
        this.destroy$.complete();
×
543
    }
544

545
    /** @hidden @internal */
546
    public getOverlayById(id: string): OverlayInfo {
547
        if (!id) {
492✔
548
            return null;
107✔
549
        }
550
        const info = this._overlayInfos.find(e => e.id === id);
610✔
551
        return info;
385✔
552
    }
553

554
    private _hide(id: string, event?: Event) {
555
        const info: OverlayInfo = this.getOverlayById(id);
58✔
556
        if (!info) {
58!
UNCOV
557
            console.warn('igxOverlay.hide was called with wrong id: ', id);
×
UNCOV
558
            return;
×
559
        }
560
        const eventArgs: OverlayClosingEventArgs = { id, componentRef: info.componentRef, cancel: false, event };
58✔
561
        this.closing.emit(eventArgs);
58✔
562
        if (eventArgs.cancel) {
58!
UNCOV
563
            return;
×
564
        }
565
        this.removeModalClasses(info);
58✔
566
        if (info.settings.positionStrategy.settings.closeAnimation) {
58!
567
            this.playCloseAnimation(info, event);
58✔
568
        } else {
UNCOV
569
            this.closeDone(info);
×
570
        }
571
    }
572

573
    /**
574
     * Creates overlayInfo. Sets the info's `elementRef`, `componentRef`and `settings`. Also
575
     * initialize info's `ngZone`, `transformX` and `transformY`.
576
     * @param component ElementRef or Type. If type is provided dynamic component will be created
577
     * @param viewContainerRefOrSettings (optional): If ElementRef is provided for `component` this
578
     * parameter is OverlaySettings. Otherwise it could be ViewContainerRef or OverlayCreateSettings and will be
579
     * used when dynamic component is created.
580
     * @param settings (optional): OverlaySettings when `ViewContainerRef` is provided.
581
     * @returns OverlayInfo
582
     */
583
    private getOverlayInfo(
584
        component: ElementRef | Type<any>,
585
        viewContainerRefOrSettings?: ViewContainerRef | OverlayCreateSettings,
586
        settings?: OverlaySettings): OverlayInfo | null {
587
        const info: OverlayInfo = { ngZone: this._zone, transformX: 0, transformY: 0 };
78✔
588
        let overlaySettings = settings;
78✔
589
        if (component instanceof ElementRef) {
78✔
590
            info.elementRef = component;
41✔
591
            overlaySettings = viewContainerRefOrSettings as OverlaySettings;
41✔
592
        } else {
593
            let dynamicComponent: ComponentRef<any>;
594
            if (viewContainerRefOrSettings instanceof ViewContainerRef) {
37!
595
                const viewContainerRef = viewContainerRefOrSettings as ViewContainerRef;
37✔
596
                dynamicComponent = viewContainerRef.createComponent(component);
37✔
597
            } else {
UNCOV
598
                const environmentInjector = this._appRef.injector;
×
UNCOV
599
                const createSettings = viewContainerRefOrSettings as OverlayCreateSettings | undefined;
×
600
                let elementInjector: Injector;
UNCOV
601
                if (createSettings) {
×
UNCOV
602
                    ({ injector: elementInjector, ...overlaySettings } = createSettings);
×
603
                }
UNCOV
604
                dynamicComponent = createComponent(component, { environmentInjector, elementInjector });
×
UNCOV
605
                this._appRef.attachView(dynamicComponent.hostView);
×
606
            }
607
            if (dynamicComponent.onDestroy) {
37✔
608
                dynamicComponent.onDestroy(() => {
37✔
609
                    if (!info.detached && this._overlayInfos.indexOf(info) !== -1) {
37✔
610
                        this.detach(info.id);
18✔
611
                    }
612
                })
613
            }
614

615
            // If the element is newly created from a Component, it is wrapped in 'ng-component' tag - we do not want that.
616
            const element = dynamicComponent.location.nativeElement;
37✔
617
            info.elementRef = { nativeElement: element };
37✔
618
            info.componentRef = dynamicComponent;
37✔
619
        }
620
        info.settings = Object.assign({}, this._defaultSettings, overlaySettings);
78✔
621
        return info;
78✔
622
    }
623

624
    private placeElementHook(element: HTMLElement): HTMLElement {
625
        if (!element.parentElement) {
78!
UNCOV
626
            return null;
×
627
        }
628
        const hook = this._document.createElement('div');
78✔
629
        hook.style.display = 'none';
78✔
630
        element.parentElement.insertBefore(hook, element);
78✔
631
        return hook;
78✔
632
    }
633

634
    private moveElementToOverlay(info: OverlayInfo) {
635
        info.wrapperElement = this.getWrapperElement();
78✔
636
        const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal);
78✔
637
        this.getOverlayElement(info).appendChild(info.wrapperElement);
78✔
638
        contentElement.appendChild(info.elementRef.nativeElement);
78✔
639
    }
640

641
    private getWrapperElement(): HTMLElement {
642
        const wrapper: HTMLElement = this._document.createElement('div');
78✔
643
        wrapper.classList.add('igx-overlay__wrapper');
78✔
644
        return wrapper;
78✔
645
    }
646

647
    private getContentElement(wrapperElement: HTMLElement, modal: boolean): HTMLElement {
648
        const content: HTMLElement = this._document.createElement('div');
78✔
649
        if (modal) {
78!
UNCOV
650
            content.classList.add('igx-overlay__content--modal');
×
UNCOV
651
            content.addEventListener('click', (ev: Event) => {
×
UNCOV
652
                ev.stopPropagation();
×
653
            });
654
        } else {
655
            content.classList.add('igx-overlay__content');
78✔
656
        }
657
        content.addEventListener('scroll', (ev: Event) => {
78✔
658
            ev.stopPropagation();
×
659
        });
660

661
        //  hide element to eliminate flickering. Show the element exactly before animation starts
662
        wrapperElement.style.visibility = 'hidden';
78✔
663
        wrapperElement.appendChild(content);
78✔
664
        return content;
78✔
665
    }
666

667
    private getOverlayElement(info: OverlayInfo): HTMLElement {
668
        if (info.settings.outlet) {
156✔
669
            return info.settings.outlet.nativeElement || info.settings.outlet;
156!
670
        }
UNCOV
671
        if (!this._overlayElement) {
×
UNCOV
672
            this._overlayElement = this._document.createElement('div');
×
UNCOV
673
            this._overlayElement.classList.add('igx-overlay');
×
UNCOV
674
            this._document.body.appendChild(this._overlayElement);
×
675
        }
UNCOV
676
        return this._overlayElement;
×
677
    }
678

679
    private updateSize(info: OverlayInfo) {
680
        if (info.componentRef) {
78✔
681
            //  if we are positioning component this is first time it gets visible
682
            //  and we can finally get its size
683
            info.componentRef.changeDetectorRef.detectChanges();
37✔
684
            info.initialSize = info.elementRef.nativeElement.getBoundingClientRect();
37✔
685
        }
686

687
        // set content div width only if element to show has width
688
        if (info.initialSize.width !== 0) {
78✔
689
            info.elementRef.nativeElement.parentElement.style.width = info.initialSize.width + 'px';
78✔
690
        }
691
    }
692

693
    private closeDone(info: OverlayInfo) {
694
        info.visible = false;
78✔
695
        if (info.wrapperElement) {
78✔
696
            // to eliminate flickering show the element just before animation start
697
            info.wrapperElement.style.visibility = 'hidden';
57✔
698
        }
699
        if (!info.closeAnimationDetaching) {
78✔
700
            this.closed.emit({ id: info.id, componentRef: info.componentRef, event: info.event });
57✔
701
        }
702
        delete info.event;
78✔
703
    }
704

705
    private cleanUp(info: OverlayInfo) {
706
        const child: HTMLElement = info.elementRef.nativeElement;
78✔
707
        const outlet = this.getOverlayElement(info);
78✔
708
        // if same element is shown in other overlay outlet will not contain
709
        // the element and we should not remove it form outlet
710
        if (outlet.contains(child)) {
78✔
711
            outlet.removeChild(child.parentNode.parentNode);
78✔
712
        }
713
        if (info.componentRef) {
78✔
714
            this._appRef.detachView(info.componentRef.hostView);
37✔
715
            info.componentRef.destroy();
37✔
716
            delete info.componentRef;
37✔
717
        }
718
        if (info.hook) {
78✔
719
            info.hook.parentElement.insertBefore(info.elementRef.nativeElement, info.hook);
78✔
720
            info.hook.parentElement.removeChild(info.hook);
78✔
721
            delete info.hook;
78✔
722
        }
723

724
        const index = this._overlayInfos.indexOf(info);
78✔
725
        this._overlayInfos.splice(index, 1);
78✔
726

727
        // this._overlayElement.parentElement check just for tests that manually delete the element
728
        if (this._overlayInfos.length === 0) {
78✔
729
            if (this._overlayElement && this._overlayElement.parentElement) {
41!
UNCOV
730
                this._overlayElement.parentElement.removeChild(this._overlayElement);
×
UNCOV
731
                this._overlayElement = null;
×
732
            }
733
            this.removeCloseOnEscapeListener();
41✔
734
        }
735

736
        // clean all the resources attached to info
737
        delete info.elementRef;
78✔
738
        delete info.settings;
78✔
739
        delete info.initialSize;
78✔
740
        info.openAnimationDetaching = true;
78✔
741
        info.openAnimationPlayer?.destroy();
78✔
742
        delete info.openAnimationPlayer;
78✔
743
        info.closeAnimationDetaching = true;
78✔
744
        info.closeAnimationPlayer?.destroy();
78✔
745
        delete info.closeAnimationPlayer;
78✔
746
        delete info.ngZone;
78✔
747
        delete info.wrapperElement;
78✔
748
        info = null;
78✔
749
    }
750

751
    private playOpenAnimation(info: OverlayInfo) {
752
        //  if there is opening animation already started do nothing
753
        if (info.openAnimationPlayer?.hasStarted()) {
78!
754
            return;
×
755
        }
756
        if (info.closeAnimationPlayer?.hasStarted()) {
78!
UNCOV
757
            const position = info.closeAnimationPlayer.position;
×
UNCOV
758
            info.closeAnimationPlayer.reset();
×
UNCOV
759
            info.openAnimationPlayer.init();
×
UNCOV
760
            info.openAnimationPlayer.position = 1 - position;
×
761
        }
762
        this.animationStarting.emit({ id: info.id, animationPlayer: info.openAnimationPlayer, animationType: 'open' });
78✔
763

764
        //  to eliminate flickering show the element just before animation start
765
        info.wrapperElement.style.visibility = '';
78✔
766
        info.visible = true;
78✔
767
        info.openAnimationPlayer.play();
78✔
768
    }
769

770
    private playCloseAnimation(info: OverlayInfo, event?: Event) {
771
        //  if there is closing animation already started do nothing
772
        if (info.closeAnimationPlayer?.hasStarted()) {
58✔
773
            return;
1✔
774
        }
775
        if (info.openAnimationPlayer?.hasStarted()) {
57✔
776
            const position = info.openAnimationPlayer.position;
40✔
777
            info.openAnimationPlayer.reset();
40✔
778
            info.closeAnimationPlayer.init();
40✔
779
            info.closeAnimationPlayer.position = 1 - position;
40✔
780
        }
781
        this.animationStarting.emit({ id: info.id, animationPlayer: info.closeAnimationPlayer, animationType: 'close' });
57✔
782
        info.event = event;
57✔
783
        info.closeAnimationPlayer.play();
57✔
784
    }
785

786
    //  TODO: check if applyAnimationParams will work with complex animations
787
    private applyAnimationParams(wrapperElement: HTMLElement, animationOptions: AnimationReferenceMetadata) {
UNCOV
788
        if (!animationOptions) {
×
UNCOV
789
            wrapperElement.style.transitionDuration = '0ms';
×
UNCOV
790
            return;
×
791
        }
UNCOV
792
        if (!animationOptions.options || !animationOptions.options.params) {
×
793
            return;
×
794
        }
UNCOV
795
        const params = animationOptions.options.params as IAnimationParams;
×
UNCOV
796
        if (params.duration) {
×
UNCOV
797
            wrapperElement.style.transitionDuration = params.duration;
×
798
        }
UNCOV
799
        if (params.easing) {
×
UNCOV
800
            wrapperElement.style.transitionTimingFunction = params.easing;
×
801
        }
802
    }
803

804
    private documentClicked = (ev: MouseEvent) => {
35✔
805
        //  if we get to modal overlay just return - we should not close anything under it
806
        //  if we get to non-modal overlay do the next:
807
        //   1. Check it has close on outside click. If not go on to next overlay;
808
        //   2. If true check if click is on the element. If it is on the element we have closed
809
        //  already all previous non-modal with close on outside click elements, so we return. If
810
        //  not close the overlay and check next
811
        for (let i = this._overlayInfos.length; i--;) {
81✔
812
            const info = this._overlayInfos[i];
82✔
813
            if (info.settings.modal) {
82!
814
                return;
×
815
            }
816
            if (info.settings.closeOnOutsideClick) {
82✔
817
                const target = ev.composed ? ev.composedPath()[0] : ev.target;
42!
818
                const overlayElement = info.elementRef.nativeElement;
42✔
819
                // check if the click is on the overlay element or on an element from the exclusion list, and if so do not close the overlay
820
                const excludeElements = info.settings.excludeFromOutsideClick ?
42!
821
                    [...info.settings.excludeFromOutsideClick, overlayElement] : [overlayElement];
822
                const isInsideClick: boolean = excludeElements.some(e => e.contains(target as Node));
79✔
823
                if (isInsideClick) {
42✔
824
                    return;
41✔
825
                    //  if the click is outside click, but close animation has started do nothing
826
                } else if (!(info.closeAnimationPlayer?.hasStarted())) {
1✔
827
                    this._hide(info.id, ev);
1✔
828
                }
829
            }
830
        }
831
    };
832

833
    private addOutsideClickListener(info: OverlayInfo) {
834
        if (info.settings.closeOnOutsideClick) {
78✔
835
            if (info.settings.modal) {
41!
UNCOV
836
                fromEvent(info.elementRef.nativeElement.parentElement.parentElement, 'click')
×
837
                    .pipe(takeUntil(this.destroy$))
UNCOV
838
                    .subscribe((e: Event) => this._hide(info.id, e));
×
839
            } else if (
41✔
840
                //  if all overlays minus closing overlays equals one add the handler
841
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal).length -
78✔
842
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal &&
78✔
843
                    x.closeAnimationPlayer?.hasStarted()).length === 1) {
844

845
                // click event is not fired on iOS. To make element "clickable" we are
846
                // setting the cursor to pointer
847
                if (this.platformUtil.isIOS && !this._cursorStyleIsSet) {
41!
UNCOV
848
                    this._cursorOriginalValue = this._document.body.style.cursor;
×
UNCOV
849
                    this._document.body.style.cursor = 'pointer';
×
UNCOV
850
                    this._cursorStyleIsSet = true;
×
851
                }
852
                this._document.addEventListener('click', this.documentClicked, true);
41✔
853
            }
854
        }
855
    }
856

857
    private removeOutsideClickListener(info: OverlayInfo) {
858
        if (info.settings.modal === false) {
78✔
859
            let shouldRemoveClickEventListener = true;
78✔
860
            this._overlayInfos.forEach(o => {
78✔
861
                if (o.settings.modal === false && o.id !== info.id) {
115✔
862
                    shouldRemoveClickEventListener = false;
37✔
863
                }
864
            });
865
            if (shouldRemoveClickEventListener) {
78✔
866
                if (this._cursorStyleIsSet) {
41!
UNCOV
867
                    this._document.body.style.cursor = this._cursorOriginalValue;
×
UNCOV
868
                    this._cursorOriginalValue = '';
×
UNCOV
869
                    this._cursorStyleIsSet = false;
×
870
                }
871
                this._document.removeEventListener('click', this.documentClicked, true);
41✔
872
            }
873
        }
874
    }
875

876
    private addResizeHandler() {
877
        const closingOverlaysCount =
878
            this._overlayInfos
78✔
879
                .filter(o => o.closeAnimationPlayer?.hasStarted())
115✔
880
                .length;
881
        if (this._overlayInfos.length - closingOverlaysCount === 1) {
78✔
882
            this._document.defaultView.addEventListener('resize', this.repositionAll);
41✔
883
        }
884
    }
885

886
    private removeResizeHandler() {
887
        const closingOverlaysCount =
888
            this._overlayInfos
78✔
889
                .filter(o => o.closeAnimationPlayer?.hasStarted())
115✔
890
                .length;
891
        if (this._overlayInfos.length - closingOverlaysCount === 1) {
78✔
892
            this._document.defaultView.removeEventListener('resize', this.repositionAll);
41✔
893
        }
894
    }
895

896
    private addCloseOnEscapeListener(info: OverlayInfo) {
897
        if (info.settings.closeOnEscape && !this._keyPressEventListener) {
78!
UNCOV
898
            this._keyPressEventListener = fromEvent(this._document, 'keydown').pipe(
×
UNCOV
899
                filter((ev: KeyboardEvent) => ev.key === 'Escape' || ev.key === 'Esc')
×
900
            ).subscribe((ev) => {
UNCOV
901
                const visibleOverlays = this._overlayInfos.filter(o => o.visible);
×
UNCOV
902
                if (visibleOverlays.length < 1) {
×
UNCOV
903
                    return;
×
904
                }
UNCOV
905
                const targetOverlayInfo = visibleOverlays[visibleOverlays.length - 1];
×
UNCOV
906
                if (targetOverlayInfo.visible && targetOverlayInfo.settings.closeOnEscape) {
×
UNCOV
907
                    this.hide(targetOverlayInfo.id, ev);
×
908
                }
909
            });
910
        }
911
    }
912

913
    private removeCloseOnEscapeListener() {
914
        if (this._keyPressEventListener) {
41!
UNCOV
915
            this._keyPressEventListener.unsubscribe();
×
UNCOV
916
            this._keyPressEventListener = null;
×
917
        }
918
    }
919

920
    private addModalClasses(info: OverlayInfo) {
921
        if (info.settings.modal) {
78!
UNCOV
922
            const wrapperElement = info.elementRef.nativeElement.parentElement.parentElement;
×
UNCOV
923
            wrapperElement.classList.remove('igx-overlay__wrapper');
×
UNCOV
924
            this.applyAnimationParams(wrapperElement, info.settings.positionStrategy.settings.openAnimation);
×
UNCOV
925
            requestAnimationFrame(() => {
×
UNCOV
926
                wrapperElement.classList.add('igx-overlay__wrapper--modal');
×
927
            });
928
        }
929
    }
930

931
    private removeModalClasses(info: OverlayInfo) {
932
        if (info.settings.modal) {
58!
UNCOV
933
            const wrapperElement = info.elementRef.nativeElement.parentElement.parentElement;
×
UNCOV
934
            this.applyAnimationParams(wrapperElement, info.settings.positionStrategy.settings.closeAnimation);
×
UNCOV
935
            wrapperElement.classList.remove('igx-overlay__wrapper--modal');
×
UNCOV
936
            wrapperElement.classList.add('igx-overlay__wrapper');
×
937
        }
938
    }
939

940
    private buildAnimationPlayers(info: OverlayInfo) {
941
        if (info.settings.positionStrategy.settings.openAnimation) {
78✔
942
            info.openAnimationPlayer = this.animationService
78✔
943
                .buildAnimation(info.settings.positionStrategy.settings.openAnimation, info.elementRef.nativeElement);
944
            info.openAnimationPlayer.animationEnd
78✔
945
                .pipe(takeUntil(this.destroy$))
946
                .subscribe(() => this.openAnimationDone(info));
99✔
947
        }
948
        if (info.settings.positionStrategy.settings.closeAnimation) {
78✔
949
            info.closeAnimationPlayer = this.animationService
78✔
950
                .buildAnimation(info.settings.positionStrategy.settings.closeAnimation, info.elementRef.nativeElement);
951
            info.closeAnimationPlayer.animationEnd
78✔
952
                .pipe(takeUntil(this.destroy$))
953
                .subscribe(() => this.closeAnimationDone(info));
78✔
954
        }
955
    }
956

957
    private openAnimationDone(info: OverlayInfo) {
958
        if (!info.openAnimationDetaching) {
99✔
959
            this.opened.emit({ id: info.id, componentRef: info.componentRef });
78✔
960
        }
961
        if (info.openAnimationPlayer) {
99✔
962
            info.openAnimationPlayer.reset();
78✔
963
        }
964
        if (info.closeAnimationPlayer?.hasStarted()) {
99✔
965
            info.closeAnimationPlayer.reset();
40✔
966
        }
967
    }
968

969
    private closeAnimationDone(info: OverlayInfo) {
970
        if (info.closeAnimationPlayer) {
78✔
971
            info.closeAnimationPlayer.reset();
57✔
972
        }
973
        if (info.openAnimationPlayer?.hasStarted()) {
78!
UNCOV
974
            info.openAnimationPlayer.reset();
×
975
        }
976
        this.closeDone(info);
78✔
977
    }
978

979
    private finishAnimations(info: OverlayInfo) {
980
        // // TODO: should we emit here opened or closed events
981
        if (info.openAnimationPlayer?.hasStarted()) {
78!
UNCOV
982
            info.openAnimationPlayer.finish();
×
983
        }
984
        if (info.closeAnimationPlayer?.hasStarted()) {
78!
UNCOV
985
            info.closeAnimationPlayer.finish();
×
986
        }
987
    }
988

989
    private getComponentSize(info: OverlayInfo) {
990
        if (info.elementRef?.nativeElement instanceof Element) {
78✔
991
            const styles = this._document.defaultView.getComputedStyle(info.elementRef.nativeElement);
78✔
992
            const componentSize = styles.getPropertyValue('--component-size');
78✔
993
            const globalSize = styles.getPropertyValue('--ig-size');
78✔
994
            const size = componentSize || globalSize;
78✔
995
            info.size = size;
78✔
996
        }
997
    }
998
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc