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

IgniteUI / igniteui-angular / 13548823408

26 Feb 2025 04:36PM CUT coverage: 91.555% (-0.04%) from 91.599%
13548823408

Pull #15223

github

web-flow
Merge 3ac8087aa into 786685675
Pull Request #15223: fix(pivot-grid): added createRow method for grid based events - 19.0

12997 of 15250 branches covered (85.23%)

0 of 18 new or added lines in 2 files covered. (0.0%)

135 existing lines in 23 files now uncovered.

26344 of 28774 relevant lines covered (91.55%)

34046.37 hits per line

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

89.92
/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>();
3,534✔
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>();
3,534✔
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>();
3,534✔
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>();
3,534✔
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>();
3,534✔
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>();
3,534✔
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>();
3,534✔
125

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

135
    private _defaultSettings: OverlaySettings = {
3,534✔
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,
3,534✔
146
        @Inject(DOCUMENT) private document: any,
3,534✔
147
        private _zone: NgZone,
3,534✔
148
        protected platformUtil: PlatformUtil,
3,534✔
149
        @Inject(IgxAngularAnimationService) private animationService: AnimationService) {
3,534✔
150
        this._document = this.document;
3,534✔
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);
1,611✔
332

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

338
        info.id = (this._componentId++).toString();
1,611✔
339
        info.visible = false;
1,611✔
340
        // Emit the contentAppending event before appending the content
341
        const eventArgs = { id: info.id, elementRef: info.elementRef, componentRef: info.componentRef, settings: info.settings };
1,611✔
342
        this.contentAppending.emit(eventArgs);
1,611✔
343
        // Append the content to the overlay
344
        info.settings = eventArgs.settings;
1,611✔
345
        this._overlayInfos.push(info);
1,611✔
346
        info.hook = this.placeElementHook(info.elementRef.nativeElement);
1,611✔
347
        const elementRect = info.elementRef.nativeElement.getBoundingClientRect();
1,611✔
348
        info.initialSize = { width: elementRect.width, height: elementRect.height };
1,611✔
349
        this.addComponentSize(info);
1,611✔
350
        this.moveElementToOverlay(info);
1,611✔
351
        this.contentAppended.emit({ id: info.id, componentRef: info.componentRef });
1,611✔
352
        info.settings.scrollStrategy.initialize(this._document, this, info.id);
1,611✔
353
        info.settings.scrollStrategy.attach();
1,611✔
354
        this.addOutsideClickListener(info);
1,611✔
355
        this.addResizeHandler();
1,611✔
356
        this.addCloseOnEscapeListener(info);
1,611✔
357
        this.buildAnimationPlayers(info);
1,611✔
358
        return info.id;
1,611✔
359
    }
360

361
    /**
362
     * Remove overlay with the provided id.
363
     *
364
     * @param id Id of the overlay to remove
365
     * ```typescript
366
     * this.overlay.detach(id);
367
     * ```
368
     */
369
    public detach(id: string) {
370
        const info: OverlayInfo = this.getOverlayById(id);
1,608✔
371

372
        if (!info) {
1,608✔
373
            console.warn('igxOverlay.detach was called with wrong id: ', id);
2✔
374
            return;
2✔
375
        }
376
        info.detached = true;
1,606✔
377
        this.finishAnimations(info);
1,606✔
378
        info.settings.scrollStrategy.detach();
1,606✔
379
        this.removeOutsideClickListener(info);
1,606✔
380
        this.removeResizeHandler();
1,606✔
381
        this.cleanUp(info);
1,606✔
382
    }
383

384
    /**
385
     * Remove all the overlays.
386
     * ```typescript
387
     * this.overlay.detachAll();
388
     * ```
389
     */
390
    public detachAll() {
391
        for (let i = this._overlayInfos.length; i--;) {
242✔
392
            this.detach(this._overlayInfos[i].id);
257✔
393
        }
394
    }
395

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

438
    /**
439
     * Hides the component with the ID provided as a parameter.
440
     * ```typescript
441
     * this.overlay.hide(id);
442
     * ```
443
     */
444
    public hide(id: string, event?: Event) {
445
        this._hide(id, event);
793✔
446
    }
447

448
    /**
449
     * Hides all the components and the overlay.
450
     * ```typescript
451
     * this.overlay.hideAll();
452
     * ```
453
     */
454
    public hideAll() {
455
        for (let i = this._overlayInfos.length; i--;) {
4✔
456
            this.hide(this._overlayInfos[i].id);
6✔
457
        }
458
    }
459

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

490
    /**
491
     * 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
492
     *
493
     * @param id Id to offset overlay for
494
     * @param deltaX Amount of offset in horizontal direction
495
     * @param deltaY Amount of offset in vertical direction
496
     * @param offsetMode Determines whether to add (by default) or set the offset values with OffsetMode.Add and OffsetMode.Set
497
     * ```typescript
498
     * this.overlay.setOffset(id, deltaX, deltaY, offsetMode);
499
     * ```
500
     */
501
    public setOffset(id: string, deltaX: number, deltaY: number, offsetMode?: OffsetMode) {
502
        const info: OverlayInfo = this.getOverlayById(id);
10✔
503

504
        if (!info) {
10✔
505
            return;
4✔
506
        }
507

508
        switch (offsetMode) {
6✔
509
            case OffsetMode.Set:
510
                info.transformX = deltaX;
1✔
511
                info.transformY = deltaY;
1✔
512
                break;
1✔
513
            case OffsetMode.Add:
514
            default:
515
                info.transformX += deltaX;
5✔
516
                info.transformY += deltaY;
5✔
517
                break;
5✔
518
        }
519

520
        const transformX = info.transformX;
6✔
521
        const transformY = info.transformY;
6✔
522

523
        const translate = `translate(${transformX}px, ${transformY}px)`;
6✔
524
        info.elementRef.nativeElement.parentElement.style.transform = translate;
6✔
525
    }
526

527
    /** @hidden */
528
    public repositionAll = () => {
3,534✔
529
        for (let i = this._overlayInfos.length; i--;) {
51✔
530
            this.reposition(this._overlayInfos[i].id);
4✔
531
        }
532
    };
533

534
    /** @hidden */
535
    public ngOnDestroy(): void {
536
        this.destroy$.next(true);
23✔
537
        this.destroy$.complete();
23✔
538
    }
539

540
    /** @hidden @internal */
541
    public getOverlayById(id: string): OverlayInfo {
542
        if (!id) {
14,415✔
543
            return null;
4,785✔
544
        }
545
        const info = this._overlayInfos.find(e => e.id === id);
9,630✔
546
        return info;
9,630✔
547
    }
548

549
    private _hide(id: string, event?: Event) {
550
        const info: OverlayInfo = this.getOverlayById(id);
1,064✔
551
        if (!info) {
1,064✔
552
            console.warn('igxOverlay.hide was called with wrong id: ', id);
28✔
553
            return;
28✔
554
        }
555
        const eventArgs: OverlayClosingEventArgs = { id, componentRef: info.componentRef, cancel: false, event };
1,036✔
556
        this.closing.emit(eventArgs);
1,036✔
557
        if (eventArgs.cancel) {
1,036✔
558
            return;
9✔
559
        }
560
        this.removeModalClasses(info);
1,027✔
561
        if (info.settings.positionStrategy.settings.closeAnimation) {
1,027✔
562
            this.playCloseAnimation(info, event);
584✔
563
        } else {
564
            this.closeDone(info);
443✔
565
        }
566
    }
567

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

610
            // If the element is newly created from a Component, it is wrapped in 'ng-component' tag - we do not want that.
611
            const element = dynamicComponent.location.nativeElement;
613✔
612
            info.elementRef = { nativeElement: element };
613✔
613
            info.componentRef = dynamicComponent;
613✔
614
        }
615
        info.settings = Object.assign({}, this._defaultSettings, overlaySettings);
1,611✔
616
        return info;
1,611✔
617
    }
618

619
    private placeElementHook(element: HTMLElement): HTMLElement {
620
        if (!element.parentElement) {
1,611✔
621
            return null;
301✔
622
        }
623
        const hook = this._document.createElement('div');
1,310✔
624
        hook.style.display = 'none';
1,310✔
625
        element.parentElement.insertBefore(hook, element);
1,310✔
626
        return hook;
1,310✔
627
    }
628

629
    private moveElementToOverlay(info: OverlayInfo) {
630
        info.wrapperElement = this.getWrapperElement();
1,611✔
631
        const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal);
1,611✔
632
        this.getOverlayElement(info).appendChild(info.wrapperElement);
1,611✔
633
        contentElement.appendChild(info.elementRef.nativeElement);
1,611✔
634
    }
635

636
    private getWrapperElement(): HTMLElement {
637
        const wrapper: HTMLElement = this._document.createElement('div');
1,611✔
638
        wrapper.classList.add('igx-overlay__wrapper');
1,611✔
639
        return wrapper;
1,611✔
640
    }
641

642
    private getContentElement(wrapperElement: HTMLElement, modal: boolean): HTMLElement {
643
        const content: HTMLElement = this._document.createElement('div');
1,611✔
644
        if (modal) {
1,611✔
645
            content.classList.add('igx-overlay__content--modal');
107✔
646
            content.addEventListener('click', (ev: Event) => {
107✔
647
                ev.stopPropagation();
4✔
648
            });
649
        } else {
650
            content.classList.add('igx-overlay__content');
1,504✔
651
        }
652
        content.addEventListener('scroll', (ev: Event) => {
1,611✔
UNCOV
653
            ev.stopPropagation();
×
654
        });
655

656
        //  hide element to eliminate flickering. Show the element exactly before animation starts
657
        wrapperElement.style.visibility = 'hidden';
1,611✔
658
        wrapperElement.appendChild(content);
1,611✔
659
        return content;
1,611✔
660
    }
661

662
    private getOverlayElement(info: OverlayInfo): HTMLElement {
663
        if (info.settings.outlet) {
3,217✔
664
            return info.settings.outlet.nativeElement || info.settings.outlet;
1,675✔
665
        }
666
        if (!this._overlayElement) {
1,542✔
667
            this._overlayElement = this._document.createElement('div');
749✔
668
            this._overlayElement.classList.add('igx-overlay');
749✔
669
            this._document.body.appendChild(this._overlayElement);
749✔
670
        }
671
        return this._overlayElement;
1,542✔
672
    }
673

674
    private updateSize(info: OverlayInfo) {
675
        // set content div size
676
        if (info.size) {
1,629✔
677
            info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size);
912✔
678
        }
679
        
680
        if (info.componentRef) {
1,629✔
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();
609✔
684
            info.initialSize = info.elementRef.nativeElement.getBoundingClientRect();
609✔
685
        }
686

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

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

705
    private cleanUp(info: OverlayInfo) {
706
        const child: HTMLElement = info.elementRef.nativeElement;
1,606✔
707
        const outlet = this.getOverlayElement(info);
1,606✔
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)) {
1,606✔
711
            outlet.removeChild(child.parentNode.parentNode);
1,603✔
712
        }
713
        if (info.componentRef) {
1,606✔
714
            this._appRef.detachView(info.componentRef.hostView);
610✔
715
            info.componentRef.destroy();
610✔
716
            delete info.componentRef;
610✔
717
        }
718
        if (info.hook) {
1,606✔
719
            info.hook.parentElement.insertBefore(info.elementRef.nativeElement, info.hook);
1,308✔
720
            info.hook.parentElement.removeChild(info.hook);
1,308✔
721
            delete info.hook;
1,308✔
722
        }
723

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

727
        // this._overlayElement.parentElement check just for tests that manually delete the element
728
        if (this._overlayInfos.length === 0) {
1,606✔
729
            if (this._overlayElement && this._overlayElement.parentElement) {
1,355✔
730
                this._overlayElement.parentElement.removeChild(this._overlayElement);
718✔
731
                this._overlayElement = null;
718✔
732
            }
733
            this.removeCloseOnEscapeListener();
1,355✔
734
        }
735

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

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

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

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

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

804
    private documentClicked = (ev: MouseEvent) => {
3,534✔
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--;) {
1,243✔
812
            const info = this._overlayInfos[i];
665✔
813
            if (info.settings.modal) {
665!
UNCOV
814
                return;
×
815
            }
816
            if (info.settings.closeOnOutsideClick) {
665✔
817
                const target = ev.composed ? ev.composedPath()[0] : ev.target;
663✔
818
                const overlayElement = info.elementRef.nativeElement;
663✔
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 ?
663!
821
                    [...info.settings.excludeFromOutsideClick, overlayElement] : [overlayElement];
822
                const isInsideClick: boolean = excludeElements.some(e => e.contains(target as Node));
758✔
823
                if (isInsideClick) {
663✔
824
                    return;
393✔
825
                    //  if the click is outside click, but close animation has started do nothing
826
                } else if (!(info.closeAnimationPlayer?.hasStarted())) {
270✔
827
                    this._hide(info.id, ev);
270✔
828
                }
829
            }
830
        }
831
    };
832

833
    private addOutsideClickListener(info: OverlayInfo) {
834
        if (info.settings.closeOnOutsideClick) {
1,611✔
835
            if (info.settings.modal) {
889✔
836
                fromEvent(info.elementRef.nativeElement.parentElement.parentElement, 'click')
81✔
837
                    .pipe(takeUntil(this.destroy$))
838
                    .subscribe((e: Event) => this._hide(info.id, e));
1✔
839
            } else if (
808✔
840
                //  if all overlays minus closing overlays equals one add the handler
841
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal).length -
987✔
842
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal &&
987✔
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) {
706✔
848
                    this._cursorOriginalValue = this._document.body.style.cursor;
1✔
849
                    this._document.body.style.cursor = 'pointer';
1✔
850
                    this._cursorStyleIsSet = true;
1✔
851
                }
852
                this._document.addEventListener('click', this.documentClicked, true);
706✔
853
            }
854
        }
855
    }
856

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

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

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

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

913
    private removeCloseOnEscapeListener() {
914
        if (this._keyPressEventListener) {
1,355✔
915
            this._keyPressEventListener.unsubscribe();
332✔
916
            this._keyPressEventListener = null;
332✔
917
        }
918
    }
919

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

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

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

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

969
    private closeAnimationDone(info: OverlayInfo) {
970
        if (info.closeAnimationPlayer) {
1,198✔
971
            info.closeAnimationPlayer.reset();
493✔
972
        }
973
        if (info.openAnimationPlayer?.hasStarted()) {
1,198✔
974
            info.openAnimationPlayer.reset();
1✔
975
        }
976
        this.closeDone(info);
1,198✔
977
    }
978

979
    private finishAnimations(info: OverlayInfo) {
980
        // // TODO: should we emit here opened or closed events
981
        if (info.openAnimationPlayer?.hasStarted()) {
1,606✔
982
            info.openAnimationPlayer.finish();
322✔
983
        }
984
        if (info.closeAnimationPlayer?.hasStarted()) {
1,606✔
985
            info.closeAnimationPlayer.finish();
57✔
986
        }
987
    }
988

989
    private addComponentSize(info: OverlayInfo) {
990
        if (info.elementRef?.nativeElement instanceof Element) {
1,611✔
991
            const styles = this._document.defaultView.getComputedStyle(info.elementRef.nativeElement);
1,608✔
992
            const componentSize = styles.getPropertyValue('--component-size');
1,608✔
993
            const globalSize = styles.getPropertyValue('--ig-size');
1,608✔
994
            const size = componentSize || globalSize;
1,608✔
995
            info.size = size;
1,608✔
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