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

IgniteUI / igniteui-angular / 13416627295

19 Feb 2025 03:46PM CUT coverage: 91.615% (+0.02%) from 91.595%
13416627295

Pull #15246

github

web-flow
Merge 2a114cdda into 10ddb05cf
Pull Request #15246: fix(excel-export): Get correct grid column collection from row island…

12987 of 15218 branches covered (85.34%)

3 of 3 new or added lines in 1 file covered. (100.0%)

380 existing lines in 31 files now uncovered.

26385 of 28800 relevant lines covered (91.61%)

34358.69 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,533✔
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,533✔
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,533✔
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,533✔
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,533✔
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,533✔
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,533✔
125

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

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

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

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

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

389
    /**
390
     * Remove all the overlays.
391
     * ```typescript
392
     * this.overlay.detachAll();
393
     * ```
394
     */
395
    public detachAll() {
396
        for (let i = this._overlayInfos.length; i--;) {
242✔
397
            this.detach(this._overlayInfos[i].id);
257✔
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);
1,629✔
409
        if (!info) {
1,629!
UNCOV
410
            console.warn('igxOverlay.show was called with wrong id: ', id);
×
UNCOV
411
            return;
×
412
        }
413
        const eventArgs: OverlayCancelableEventArgs = { id, componentRef: info.componentRef, cancel: false };
1,629✔
414
        this.opening.emit(eventArgs);
1,629✔
415
        if (eventArgs.cancel) {
1,629✔
416
            return;
2✔
417
        }
418
        if (settings) {
1,627✔
419
            settings.positionStrategy ??= info.settings.positionStrategy;
973✔
420
            settings.scrollStrategy ??= info.settings.scrollStrategy;
973✔
421
            info.settings = { ...info.settings, ...settings };
973✔
422
        }
423
        this.updateSize(info);
1,627✔
424
        info.settings.positionStrategy.position(
1,627✔
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);
1,627✔
431
        if (info.settings.positionStrategy.settings.openAnimation) {
1,627✔
432
            // TODO: should we build players again. This was already done in attach!!!
433
            // this.buildAnimationPlayers(info);
434
            this.playOpenAnimation(info);
1,612✔
435
        } else {
436
            //  to eliminate flickering show the element just before opened fires
437
            info.wrapperElement.style.visibility = '';
15✔
438
            info.visible = true;
15✔
439
            this.opened.emit({ id: info.id, componentRef: info.componentRef });
15✔
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);
793✔
451
    }
452

453
    /**
454
     * Hides all the components and the overlay.
455
     * ```typescript
456
     * this.overlay.hideAll();
457
     * ```
458
     */
459
    public hideAll() {
460
        for (let i = this._overlayInfos.length; i--;) {
4✔
461
            this.hide(this._overlayInfos[i].id);
6✔
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) {
474
        const overlayInfo = this.getOverlayById(id);
178✔
475
        if (!overlayInfo || !overlayInfo.settings) {
178✔
476
            console.warn('Wrong id provided in overlay.reposition method. Id: ', id);
21✔
477
            return;
21✔
478
        }
479
        if (!overlayInfo.visible) {
157✔
480
            return;
60✔
481
        }
482
        const contentElement = overlayInfo.elementRef.nativeElement.parentElement;
97✔
483
        const contentElementRect = contentElement.getBoundingClientRect();
97✔
484
        overlayInfo.settings.positionStrategy.position(
97✔
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) {
507
        const info: OverlayInfo = this.getOverlayById(id);
10✔
508

509
        if (!info) {
10✔
510
            return;
4✔
511
        }
512

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

525
        const transformX = info.transformX;
6✔
526
        const transformY = info.transformY;
6✔
527

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

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

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

545
    /** @hidden @internal */
546
    public getOverlayById(id: string): OverlayInfo {
547
        if (!id) {
14,386✔
548
            return null;
4,788✔
549
        }
550
        const info = this._overlayInfos.find(e => e.id === id);
9,598✔
551
        return info;
9,598✔
552
    }
553

554
    private _hide(id: string, event?: Event) {
555
        const info: OverlayInfo = this.getOverlayById(id);
1,063✔
556
        if (!info) {
1,063✔
557
            console.warn('igxOverlay.hide was called with wrong id: ', id);
28✔
558
            return;
28✔
559
        }
560
        const eventArgs: OverlayClosingEventArgs = { id, componentRef: info.componentRef, cancel: false, event };
1,035✔
561
        this.closing.emit(eventArgs);
1,035✔
562
        if (eventArgs.cancel) {
1,035✔
563
            return;
9✔
564
        }
565
        this.removeModalClasses(info);
1,026✔
566
        if (info.settings.positionStrategy.settings.closeAnimation) {
1,026✔
567
            this.playCloseAnimation(info, event);
584✔
568
        } else {
569
            this.closeDone(info);
442✔
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 };
1,609✔
588
        let overlaySettings = settings;
1,609✔
589
        if (component instanceof ElementRef) {
1,609✔
590
            info.elementRef = component;
996✔
591
            overlaySettings = viewContainerRefOrSettings as OverlaySettings;
996✔
592
        } else {
593
            let dynamicComponent: ComponentRef<any>;
594
            if (viewContainerRefOrSettings instanceof ViewContainerRef) {
613✔
595
                const viewContainerRef = viewContainerRefOrSettings as ViewContainerRef;
322✔
596
                dynamicComponent = viewContainerRef.createComponent(component);
322✔
597
            } else {
598
                const environmentInjector = this._appRef.injector;
291✔
599
                const createSettings = viewContainerRefOrSettings as OverlayCreateSettings | undefined;
291✔
600
                let elementInjector: Injector;
601
                if (createSettings) {
291✔
602
                    ({ injector: elementInjector, ...overlaySettings } = createSettings);
251✔
603
                }
604
                dynamicComponent = createComponent(component, { environmentInjector, elementInjector });
291✔
605
                this._appRef.attachView(dynamicComponent.hostView);
291✔
606
            }
607
            if (dynamicComponent.onDestroy) {
613✔
608
                dynamicComponent.onDestroy(() => {
612✔
609
                    if (!info.detached && this._overlayInfos.indexOf(info) !== -1) {
609✔
610
                        this.detach(info.id);
186✔
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;
613✔
617
            info.elementRef = { nativeElement: element };
613✔
618
            info.componentRef = dynamicComponent;
613✔
619
        }
620
        info.settings = Object.assign({}, this._defaultSettings, overlaySettings);
1,609✔
621
        return info;
1,609✔
622
    }
623

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

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

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

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

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

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

679
    private updateSize(info: OverlayInfo) {
680
        if (info.componentRef) {
1,627✔
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,627✔
689
            info.elementRef.nativeElement.parentElement.style.width = info.initialSize.width + 'px';
1,496✔
690
        }
691
    }
692

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

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

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

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

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

751
    private playOpenAnimation(info: OverlayInfo) {
752
        //  if there is opening animation already started do nothing
753
        if (info.openAnimationPlayer?.hasStarted()) {
1,612!
UNCOV
754
            return;
×
755
        }
756
        if (info.closeAnimationPlayer?.hasStarted()) {
1,612✔
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,612✔
763

764
        //  to eliminate flickering show the element just before animation start
765
        info.wrapperElement.style.visibility = '';
1,612✔
766
        info.visible = true;
1,612✔
767
        info.openAnimationPlayer.play();
1,612✔
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,533✔
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,241✔
812
            const info = this._overlayInfos[i];
664✔
813
            if (info.settings.modal) {
664!
UNCOV
814
                return;
×
815
            }
816
            if (info.settings.closeOnOutsideClick) {
664✔
817
                const target = ev.composed ? ev.composedPath()[0] : ev.target;
662✔
818
                const overlayElement = info.elementRef.nativeElement;
662✔
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 ?
662!
821
                    [...info.settings.excludeFromOutsideClick, overlayElement] : [overlayElement];
822
                const isInsideClick: boolean = excludeElements.some(e => e.contains(target as Node));
757✔
823
                if (isInsideClick) {
662✔
824
                    return;
393✔
825
                    //  if the click is outside click, but close animation has started do nothing
826
                } else if (!(info.closeAnimationPlayer?.hasStarted())) {
269✔
827
                    this._hide(info.id, ev);
269✔
828
                }
829
            }
830
        }
831
    };
832

833
    private addOutsideClickListener(info: OverlayInfo) {
834
        if (info.settings.closeOnOutsideClick) {
1,609✔
835
            if (info.settings.modal) {
887✔
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 (
806✔
840
                //  if all overlays minus closing overlays equals one add the handler
841
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal).length -
985✔
842
                this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal &&
985✔
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) {
704✔
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);
704✔
853
            }
854
        }
855
    }
856

857
    private removeOutsideClickListener(info: OverlayInfo) {
858
        if (info.settings.modal === false) {
1,604✔
859
            let shouldRemoveClickEventListener = true;
1,502✔
860
            this._overlayInfos.forEach(o => {
1,502✔
861
                if (o.settings.modal === false && o.id !== info.id) {
1,803✔
862
                    shouldRemoveClickEventListener = false;
301✔
863
                }
864
            });
865
            if (shouldRemoveClickEventListener) {
1,502✔
866
                if (this._cursorStyleIsSet) {
1,259✔
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,259✔
872
            }
873
        }
874
    }
875

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

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

896
    private addCloseOnEscapeListener(info: OverlayInfo) {
897
        if (info.settings.closeOnEscape && !this._keyPressEventListener) {
1,609✔
898
            this._keyPressEventListener = fromEvent(this._document, 'keydown').pipe(
334✔
899
                filter((ev: KeyboardEvent) => ev.key === 'Escape' || ev.key === 'Esc')
128✔
900
            ).subscribe((ev) => {
901
                const visibleOverlays = this._overlayInfos.filter(o => o.visible);
30✔
902
                if (visibleOverlays.length < 1) {
24✔
903
                    return;
11✔
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,353✔
915
            this._keyPressEventListener.unsubscribe();
332✔
916
            this._keyPressEventListener = null;
332✔
917
        }
918
    }
919

920
    private addModalClasses(info: OverlayInfo) {
921
        if (info.settings.modal) {
1,627✔
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,026✔
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,609✔
942
            info.openAnimationPlayer = this.animationService
1,594✔
943
                .buildAnimation(info.settings.positionStrategy.settings.openAnimation, info.elementRef.nativeElement);
944
            info.openAnimationPlayer.animationEnd
1,594✔
945
                .pipe(takeUntil(this.destroy$))
946
                .subscribe(() => this.openAnimationDone(info));
2,338✔
947
        }
948
        if (info.settings.positionStrategy.settings.closeAnimation) {
1,609✔
949
            info.closeAnimationPlayer = this.animationService
1,177✔
950
                .buildAnimation(info.settings.positionStrategy.settings.closeAnimation, info.elementRef.nativeElement);
951
            info.closeAnimationPlayer.animationEnd
1,177✔
952
                .pipe(takeUntil(this.destroy$))
953
                .subscribe(() => this.closeAnimationDone(info));
1,196✔
954
        }
955
    }
956

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

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

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

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