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

IgniteUI / igniteui-angular / 24454207199

15 Apr 2026 12:22PM UTC coverage: 90.143% (-0.03%) from 90.174%
24454207199

push

github

web-flow
Merge pull request #17160 from IgniteUI/mvenkov/remove-internal-overlay-outlets

refactor(overlay): do not use outlet internally

14832 of 17276 branches covered (85.85%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 2 files covered. (100.0%)

11 existing lines in 6 files now uncovered.

29879 of 32324 relevant lines covered (92.44%)

34643.4 hits per line

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

97.52
/projects/igniteui-angular/directives/src/directives/toggle/toggle.directive.ts
1
import {
2
    ChangeDetectorRef,
3
    Directive,
4
    ElementRef,
5
    EventEmitter,
6
    HostListener,
7
    inject,
8
    Input,
9
    OnDestroy,
10
    OnInit,
11
    Output,
12
} from '@angular/core';
13
import { AbsoluteScrollStrategy, IgxOverlayOutletDirective } from 'igniteui-angular/core';
14
import { CancelableBrowserEventArgs, IBaseEventArgs, PlatformUtil } from 'igniteui-angular/core';
15
import { ConnectedPositioningStrategy } from 'igniteui-angular/core';
16
import { filter, first, takeUntil } from 'rxjs/operators';
17
import { IgxNavigationService, IToggleView } from 'igniteui-angular/core';
18
import { IgxOverlayService } from 'igniteui-angular/core';
19
import { IPositionStrategy } from 'igniteui-angular/core';
20
import { OffsetMode, OverlayClosingEventArgs, OverlayEventArgs, OverlaySettings } from 'igniteui-angular/core';
21
import { Subscription, Subject, MonoTypeOperatorFunction } from 'rxjs';
22

23
export interface ToggleViewEventArgs extends IBaseEventArgs {
24
    /** Id of the toggle view */
25
    id: string;
26
    /* blazorSuppress */
27
    event?: Event;
28
}
29

30
export interface ToggleViewCancelableEventArgs extends ToggleViewEventArgs, CancelableBrowserEventArgs { }
31

32
@Directive({
33
    exportAs: 'toggle',
34
    selector: '[igxToggle]',
35
    standalone: true,
36
    host: {
37
        '[class.igx-toggle--hidden]': 'hiddenClass',
38
        '[attr.aria-hidden]': 'hiddenClass',
39
        '[class.igx-toggle--hidden-webkit]': 'hiddenWebkitClass',
40
        '[class.igx-toggle]': 'defaultClass'
41
    }
42
})
43
export class IgxToggleDirective implements IToggleView, OnInit, OnDestroy {
3✔
44
    private elementRef = inject(ElementRef);
12,232✔
45
    private cdr = inject(ChangeDetectorRef);
12,232✔
46
    protected overlayService = inject<IgxOverlayService>(IgxOverlayService);
12,232✔
47
    private navigationService = inject(IgxNavigationService, { optional: true });
12,232✔
48
    private platform = inject(PlatformUtil, { optional: true });
12,232✔
49

50
    /**
51
     * Emits an event after the toggle container is opened.
52
     *
53
     * ```typescript
54
     * onToggleOpened(event) {
55
     *    alert("Toggle opened!");
56
     * }
57
     * ```
58
     *
59
     * ```html
60
     * <div
61
     *   igxToggle
62
     *   (opened)='onToggleOpened($event)'>
63
     * </div>
64
     * ```
65
     */
66
    @Output()
67
    public opened = new EventEmitter<ToggleViewEventArgs>();
12,232✔
68

69
    /**
70
     * Emits an event before the toggle container is opened.
71
     *
72
     * ```typescript
73
     * onToggleOpening(event) {
74
     *  alert("Toggle opening!");
75
     * }
76
     * ```
77
     *
78
     * ```html
79
     * <div
80
     *   igxToggle
81
     *   (opening)='onToggleOpening($event)'>
82
     * </div>
83
     * ```
84
     */
85
    @Output()
86
    public opening = new EventEmitter<ToggleViewCancelableEventArgs>();
12,232✔
87

88
    /**
89
     * Emits an event after the toggle container is closed.
90
     *
91
     * ```typescript
92
     * onToggleClosed(event) {
93
     *  alert("Toggle closed!");
94
     * }
95
     * ```
96
     *
97
     * ```html
98
     * <div
99
     *   igxToggle
100
     *   (closed)='onToggleClosed($event)'>
101
     * </div>
102
     * ```
103
     */
104
    @Output()
105
    public closed = new EventEmitter<ToggleViewEventArgs>();
12,232✔
106

107
    /**
108
     * Emits an event before the toggle container is closed.
109
     *
110
     * ```typescript
111
     * onToggleClosing(event) {
112
     *  alert("Toggle closing!");
113
     * }
114
     * ```
115
     *
116
     * ```html
117
     * <div
118
     *  igxToggle
119
     *  (closing)='onToggleClosing($event)'>
120
     * </div>
121
     * ```
122
     */
123
    @Output()
124
    public closing = new EventEmitter<ToggleViewCancelableEventArgs>();
12,232✔
125

126
    /**
127
     * Emits an event after the toggle element is appended to the overlay container.
128
     *
129
     * ```typescript
130
     * onAppended() {
131
     *  alert("Content appended!");
132
     * }
133
     * ```
134
     *
135
     * ```html
136
     * <div
137
     *   igxToggle
138
     *   (appended)='onToggleAppended()'>
139
     * </div>
140
     * ```
141
     */
142
    @Output()
143
    public appended = new EventEmitter<ToggleViewEventArgs>();
12,232✔
144

145
    /**
146
     * @hidden
147
     */
148
    public get collapsed(): boolean {
149
        return this._collapsed;
1,063,395✔
150
    }
151

152
    /**
153
     * Identifier which is registered into `IgxNavigationService`
154
     *
155
     * ```typescript
156
     * let myToggleId = this.toggle.id;
157
     * ```
158
     */
159
    @Input()
160
    public id: string;
161

162
    /**
163
     * @hidden
164
     */
165
    public get element(): HTMLElement {
166
        return this.elementRef.nativeElement;
1,773✔
167
    }
168

169
    /**
170
     * @hidden
171
     */
172
    public get hiddenClass() {
173
        return this.collapsed;
352,250✔
174
    }
175

176
    /**
177
     * @hidden
178
     */
179
    public get hiddenWebkitClass() {
180
        const isSafari = this.platform?.isSafari;
204,069✔
181
        const browserVersion = this.platform?.browserVersion;
204,069✔
182

183
        return this.collapsed && isSafari && !!browserVersion && browserVersion < 17.5;
204,069!
184
    }
185

186
    /**
187
     * @hidden
188
     */
189
    public get defaultClass() {
190
        return !this.collapsed;
176,125✔
191
    }
192

193
    protected _overlayId: string;
194

195
    private _collapsed = true;
12,232✔
196
    protected destroy$ = new Subject<boolean>();
12,232✔
197
    private _overlaySubFilter: [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs>] = [
12,232✔
198
        filter(x => x.id === this._overlayId),
2,271✔
199
        takeUntil(this.destroy$)
200
    ];
201
    private _overlayOpenedSub: Subscription;
202
    private _overlayClosingSub: Subscription;
203
    private _overlayClosedSub: Subscription;
204
    private _overlayContentAppendedSub: Subscription;
205

206
    /**
207
     * Opens the toggle.
208
     *
209
     * ```typescript
210
     * this.myToggle.open();
211
     * ```
212
     */
213
    public open(overlaySettings?: OverlaySettings) {
214
        //  if there is open animation do nothing
215
        //  if toggle is not collapsed and there is no close animation do nothing
216
        const info = this.overlayService.getOverlayById(this._overlayId);
1,180✔
217
        const openAnimationStarted = info?.openAnimationPlayer?.hasStarted() ?? false;
1,180✔
218
        const closeAnimationStarted = info?.closeAnimationPlayer?.hasStarted() ?? false;
1,180✔
219
        if (openAnimationStarted || !(this._collapsed || closeAnimationStarted)) {
1,180✔
220
            return;
15✔
221
        }
222

223
        this._collapsed = false;
1,165✔
224

225
        // TODO: this is a workaround for the issue introduced by Angular's with Ivy renderer.
226
        // When calling detectChanges(), Angular marks the element for check, but does not update the classes
227
        // immediately, which causes the overlay to calculate incorrect dimensions of target element.
228
        // Overlay show should be called in the next tick to ensure the classes are updated and target element is measured correctly.
229
        // Note: across the codebase, each host binding should be checked and similar fix applied if needed!!!
230
        this.elementRef.nativeElement.className = this.elementRef.nativeElement.className.replace('igx-toggle--hidden', 'igx-toggle');
1,165✔
231
        this.elementRef.nativeElement.className = this.elementRef.nativeElement.className.replace('igx-toggle--hidden-webkit', 'igx-toggle');
1,165✔
232
        this.elementRef.nativeElement.removeAttribute('aria-hidden');
1,165✔
233

234
        this.cdr.detectChanges();
1,165✔
235

236
        if (!info) {
1,165✔
237
            this.unsubscribe();
1,128✔
238
            this.subscribe();
1,128✔
239
            this._overlayId = this.overlayService.attach(this.elementRef, overlaySettings);
1,128✔
240
        }
241

242
        const args: ToggleViewCancelableEventArgs = { cancel: false, owner: this, id: this._overlayId };
1,165✔
243
        this.opening.emit(args);
1,165✔
244
        if (args.cancel) {
1,165✔
245
            this.unsubscribe();
3✔
246
            this.overlayService.detach(this._overlayId);
3✔
247
            this._collapsed = true;
3✔
248
            delete this._overlayId;
3✔
249
            this.cdr.detectChanges();
3✔
250
            return;
3✔
251
        }
252
        this.overlayService.show(this._overlayId, overlaySettings);
1,162✔
253
    }
254

255
    /**
256
     * Closes the toggle.
257
     *
258
     * ```typescript
259
     * this.myToggle.close();
260
     * ```
261
     */
262
    public close(event?: Event) {
263
        //  if toggle is collapsed do nothing
264
        //  if there is close animation do nothing, toggle will close anyway
265
        const info = this.overlayService.getOverlayById(this._overlayId);
4,332✔
266
        const closeAnimationStarted = info?.closeAnimationPlayer?.hasStarted() || false;
4,332✔
267
        if (this._collapsed || closeAnimationStarted) {
4,332✔
268
            return;
3,523✔
269
        }
270

271
        this.overlayService.hide(this._overlayId, event);
809✔
272
    }
273

274
    /**
275
     * Opens or closes the toggle, depending on its current state.
276
     *
277
     * ```typescript
278
     * this.myToggle.toggle();
279
     * ```
280
     */
281
    public toggle(overlaySettings?: OverlaySettings) {
282
        //  if toggle is collapsed call open
283
        //  if there is running close animation call open
284
        if (this.collapsed || this.isClosing) {
38✔
285
            this.open(overlaySettings);
31✔
286
        } else {
287
            this.close();
7✔
288
        }
289
    }
290

291
    /** @hidden @internal */
292
    public get isClosing() {
293
        const info = this.overlayService.getOverlayById(this._overlayId);
67✔
294
        return info ? info.closeAnimationPlayer?.hasStarted() : false;
67!
295
    }
296

297
    /**
298
     * Returns the id of the overlay the content is rendered in.
299
     * ```typescript
300
     * this.myToggle.overlayId;
301
     * ```
302
     */
303
    public get overlayId() {
304
        return this._overlayId;
1,009✔
305
    }
306

307
    /**
308
     * Repositions the toggle.
309
     * ```typescript
310
     * this.myToggle.reposition();
311
     * ```
312
     */
313
    public reposition() {
314
        this.overlayService.reposition(this._overlayId);
55✔
315
    }
316

317
    /**
318
     * Offsets the content along the corresponding axis by the provided amount with optional
319
     * offsetMode that determines whether to add (by default) or set the offset values with OffsetMode.Add and OffsetMode.Set
320
     */
321
    public setOffset(deltaX: number, deltaY: number, offsetMode?: OffsetMode) {
322
        this.overlayService.setOffset(this._overlayId, deltaX, deltaY, offsetMode);
4✔
323
    }
324

325
    /**
326
     * @hidden
327
     */
328
    public ngOnInit() {
329
        if (this.navigationService && this.id) {
8,372✔
330
            this.navigationService.add(this.id, this);
916✔
331
        }
332
    }
333

334
    /**
335
     * @hidden
336
     */
337
    public ngOnDestroy() {
338
        if (this.navigationService && this.id) {
12,182✔
339
            this.navigationService.remove(this.id);
4,713✔
340
        }
341
        if (this._overlayId) {
12,182✔
342
            this.overlayService.detach(this._overlayId);
443✔
343
        }
344
        this.unsubscribe();
12,182✔
345
        this.destroy$.next(true);
12,182✔
346
        this.destroy$.complete();
12,182✔
347
    }
348

349
    private overlayClosed = (e) => {
12,232✔
350
        this._collapsed = true;
682✔
351
        this.cdr.detectChanges();
682✔
352
        this.unsubscribe();
682✔
353
        this.overlayService.detach(this.overlayId);
682✔
354
        const args: ToggleViewEventArgs = { owner: this, id: this._overlayId, event: e.event };
682✔
355
        delete this._overlayId;
682✔
356
        this.closed.emit(args);
682✔
357
        this.cdr.markForCheck();
682✔
358
    };
359

360
    private subscribe() {
361
        this._overlayContentAppendedSub = this.overlayService
1,128✔
362
            .contentAppended
363
            .pipe(first(), takeUntil(this.destroy$))
364
            .subscribe(() => {
365
                const args: ToggleViewEventArgs = { owner: this, id: this._overlayId };
1,128✔
366
                this.appended.emit(args);
1,128✔
367
            });
368

369
        this._overlayOpenedSub = this.overlayService
1,128✔
370
            .opened
371
            .pipe(...this._overlaySubFilter)
372
            .subscribe(() => {
373
                const args: ToggleViewEventArgs = { owner: this, id: this._overlayId };
769✔
374
                this.opened.emit(args);
769✔
375
            });
376

377
        this._overlayClosingSub = this.overlayService
1,128✔
378
            .closing
379
            .pipe(...this._overlaySubFilter)
380
            .subscribe((e: OverlayClosingEventArgs) => {
381
                const args: ToggleViewCancelableEventArgs = { cancel: false, event: e.event, owner: this, id: this._overlayId };
734✔
382
                this.closing.emit(args);
734✔
383
                e.cancel = args.cancel;
734✔
384

385
                //  in case event is not canceled this will close the toggle and we need to unsubscribe.
386
                //  Otherwise if for some reason, e.g. close on outside click, close() gets called before
387
                //  closed was fired we will end with calling closing more than once
388
                if (!e.cancel) {
734✔
389
                    this.clearSubscription(this._overlayClosingSub);
729✔
390
                }
391
            });
392

393
        this._overlayClosedSub = this.overlayService
1,128✔
394
            .closed
395
            .pipe(...this._overlaySubFilter)
396
            .subscribe(this.overlayClosed);
397
    }
398

399
    private unsubscribe() {
400
        this.clearSubscription(this._overlayOpenedSub);
13,995✔
401
        this.clearSubscription(this._overlayClosingSub);
13,995✔
402
        this.clearSubscription(this._overlayClosedSub);
13,995✔
403
        this.clearSubscription(this._overlayContentAppendedSub);
13,995✔
404
    }
405

406
    private clearSubscription(subscription: Subscription) {
407
        if (subscription && !subscription.closed) {
56,709✔
408
            subscription.unsubscribe();
3,384✔
409
        }
410
    }
411
}
412

413
@Directive({
414
    exportAs: 'toggle-action',
415
    selector: '[igxToggleAction]',
416
    standalone: true
417
})
418
export class IgxToggleActionDirective implements OnInit {
3✔
419
    protected element = inject(ElementRef);
916✔
420
    protected navigationService = inject(IgxNavigationService, { optional: true });
916✔
421

422
    /**
423
     * Provide settings that control the toggle overlay positioning, interaction and scroll behavior.
424
     * ```typescript
425
     * const settings: OverlaySettings = {
426
     *      closeOnOutsideClick: false,
427
     *      modal: false
428
     *  }
429
     * ```
430
     * ---
431
     * ```html
432
     * <!--set-->
433
     * <div igxToggleAction [overlaySettings]="settings"></div>
434
     * ```
435
     */
436
    @Input()
437
    public overlaySettings: OverlaySettings;
438

439
    /**
440
     * Determines where the toggle element overlay should be attached.
441
     *
442
     * ```html
443
     * <!--set-->
444
     * <div igxToggleAction [igxToggleOutlet]="outlet"></div>
445
     * ```
446
     * Where `outlet` in an instance of `IgxOverlayOutletDirective` or an `ElementRef`
447
     *
448
     * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document
449
     * body by default, so using outlet is also no longer needed - just define the overlay in the intended
450
     * DOM tree position instead or use `container` property instead.
451
     */
452
    @Input('igxToggleOutlet')
453
    public outlet: IgxOverlayOutletDirective | ElementRef;
454

455
    /**
456
     * @hidden
457
     */
458
    @Input('igxToggleAction')
459
    public set target(target: any) {
460
        if (target !== null && target !== '') {
29✔
461
            this._target = target;
29✔
462
        }
463
    }
464

465
    /**
466
     * @hidden
467
     */
468
    public get target(): any {
469
        if (typeof this._target === 'string') {
25✔
470
            return this.navigationService.get(this._target);
9✔
471
        }
472
        return this._target;
16✔
473
    }
474

475
    protected _overlayDefaults: OverlaySettings;
476
    protected _target: IToggleView | string;
477

478
    /**
479
     * @hidden
480
     */
481
    @HostListener('click')
482
    public onClick() {
483
        if (this.outlet) {
24!
UNCOV
484
            this._overlayDefaults.outlet = this.outlet;
×
485
        }
486

487
        const clonedSettings = Object.assign({}, this._overlayDefaults, this.overlaySettings);
24✔
488
        this.updateOverlaySettings(clonedSettings);
24✔
489
        this.target.toggle(clonedSettings);
24✔
490
    }
491

492
    /**
493
     * @hidden
494
     */
495
    public ngOnInit() {
496
        const targetElement = this.element.nativeElement;
916✔
497
        this._overlayDefaults = {
916✔
498
            target: targetElement,
499
            positionStrategy: new ConnectedPositioningStrategy(),
500
            scrollStrategy: new AbsoluteScrollStrategy(),
501
            closeOnOutsideClick: true,
502
            modal: false,
503
            excludeFromOutsideClick: [targetElement as HTMLElement]
504
        };
505
    }
506

507
    /**
508
     * Updates provided overlay settings
509
     *
510
     * @param settings settings to update
511
     * @returns returns updated copy of provided overlay settings
512
     */
513
    protected updateOverlaySettings(settings: OverlaySettings): OverlaySettings {
514
        if (settings && settings.positionStrategy) {
24✔
515
            const positionStrategyClone: IPositionStrategy = settings.positionStrategy.clone();
24✔
516
            settings.target = this.element.nativeElement;
24✔
517
            settings.positionStrategy = positionStrategyClone;
24✔
518
        }
519

520
        return settings;
24✔
521
    }
522
}
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

© 2026 Coveralls, Inc