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

IgniteUI / igniteui-angular / 29568001031

17 Jul 2026 08:53AM UTC coverage: 90.137% (+0.002%) from 90.135%
29568001031

Pull #17328

github

web-flow
Merge 001d30684 into 8ebabbb38
Pull Request #17328: fix(grid): fix zone.onStable patterns broken in zoneless change detection

14906 of 17373 branches covered (85.8%)

Branch coverage included in aggregate %.

29 of 31 new or added lines in 7 files covered. (93.55%)

40 existing lines in 10 files now uncovered.

29984 of 32429 relevant lines covered (92.46%)

37356.35 hits per line

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

90.22
/projects/igniteui-angular/splitter/src/splitter/splitter.component.ts
1
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, booleanAttribute, forwardRef, DOCUMENT, inject, ChangeDetectionStrategy } from '@angular/core';
2
import { DragDirection, IDragMoveEventArgs, IDragStartEventArgs, IgxDragDirective, IgxDragIgnoreDirective } from 'igniteui-angular/directives';
3
import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component';
4

5
/**
6
 * An enumeration that defines the `SplitterComponent` panes orientation.
7
 */
8
export enum SplitterType {
3✔
9
    Horizontal,
3✔
10
    Vertical
3✔
11
}
12

13
export declare interface ISplitterBarResizeEventArgs {
14
    pane: IgxSplitterPaneComponent;
15
    sibling: IgxSplitterPaneComponent;
16
}
17

18
/**
19
 * Provides a framework for a simple layout, splitting the view horizontally or vertically
20
 * into multiple smaller resizable and collapsible areas.
21
 *
22
 * @igxModule IgxSplitterModule
23
 *
24
 * @igxParent Layouts
25
 *
26
 * @igxTheme igx-splitter-theme
27
 *
28
 * @igxKeywords splitter panes layout
29
 *
30
 * @igxGroup presentation
31
 *
32
 * @example
33
 * ```html
34
 * <igx-splitter>
35
 *  <igx-splitter-pane>
36
 *      ...
37
 *  </igx-splitter-pane>
38
 *  <igx-splitter-pane>
39
 *      ...
40
 *  </igx-splitter-pane>
41
 * </igx-splitter>
42
 * ```
43
 */
44
@Component({
45
    selector: 'igx-splitter',
46
    templateUrl: './splitter.component.html',
47
    changeDetection: ChangeDetectionStrategy.Eager,
48
    imports: [forwardRef(() => IgxSplitBarComponent)]
124✔
49
})
50
export class IgxSplitterComponent implements AfterContentInit {
3✔
51
    public document = inject(DOCUMENT);
25✔
52
    private elementRef = inject(ElementRef);
25✔
53

54
    /**
55
     * Gets the list of splitter panes.
56
     *
57
     * @example
58
     * ```typescript
59
     * const panes = this.splitter.panes;
60
     * ```
61
     */
62
    @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent })
63
    public panes!: QueryList<IgxSplitterPaneComponent>;
64

65
    /**
66
    * @hidden
67
    * @internal
68
    */
69
    @HostBinding('class.igx-splitter')
70
    public cssClass = 'igx-splitter';
25✔
71

72
    /**
73
     * @hidden @internal
74
     * Gets/Sets the `overflow` property of the current splitter.
75
     */
76
    @HostBinding('style.overflow')
77
    public overflow = 'hidden';
25✔
78

79
    /**
80
     * @hidden @internal
81
     * Sets/Gets the `display` property of the current splitter.
82
     */
83
    @HostBinding('style.display')
84
    public display = 'flex';
25✔
85

86
    /**
87
     * @hidden
88
     * @internal
89
     */
90
    @HostBinding('attr.aria-orientation')
91
    public get orientation() {
92
        return this.type === SplitterType.Horizontal ? 'horizontal' : 'vertical';
175✔
93
    }
94

95
    /**
96
     * Event fired when resizing of panes starts.
97
     *
98
     * @example
99
     * ```html
100
     * <igx-splitter (resizeStart)='resizeStart($event)'>
101
     *  <igx-splitter-pane>...</igx-splitter-pane>
102
     * </igx-splitter>
103
     * ```
104
     */
105
    @Output()
106
    public resizeStart = new EventEmitter<ISplitterBarResizeEventArgs>();
25✔
107

108
    /**
109
     * Event fired when resizing of panes is in progress.
110
     *
111
     * @example
112
     * ```html
113
     * <igx-splitter (resizing)='resizing($event)'>
114
     *  <igx-splitter-pane>...</igx-splitter-pane>
115
     * </igx-splitter>
116
     * ```
117
     */
118
    @Output()
119
    public resizing = new EventEmitter<ISplitterBarResizeEventArgs>();
25✔
120

121

122
    /**
123
     * Event fired when resizing of panes ends.
124
     *
125
     * @example
126
     * ```html
127
     * <igx-splitter (resizeEnd)='resizeEnd($event)'>
128
     *  <igx-splitter-pane>...</igx-splitter-pane>
129
     * </igx-splitter>
130
     * ```
131
     */
132
    @Output()
133
    public resizeEnd = new EventEmitter<ISplitterBarResizeEventArgs>();
25✔
134

135
    private _type: SplitterType = SplitterType.Horizontal;
25✔
136

137
    /**
138
     * @hidden @internal
139
     * A field that holds the initial size of the main splitter pane in each pair of panes divided by a splitter bar.
140
     */
141
    private initialPaneSize!: number;
142

143
    /**
144
     * @hidden @internal
145
     * A field that holds the initial size of the sibling pane in each pair of panes divided by a gripper.
146
     * @memberof SplitterComponent
147
     */
148
    private initialSiblingSize!: number;
149

150
    /**
151
     * @hidden @internal
152
     * The main pane in each pair of panes divided by a gripper.
153
     */
154
    private pane!: IgxSplitterPaneComponent;
155

156
    /**
157
     * The sibling pane in each pair of panes divided by a splitter bar.
158
     */
159
    private sibling!: IgxSplitterPaneComponent;
160
    /**
161
     * Gets/Sets the splitter orientation.
162
     *
163
     * @example
164
     * ```html
165
     * <igx-splitter [type]="type">...</igx-splitter>
166
     * ```
167
     */
168
    @Input()
169
    public get type() {
170
        return this._type;
708✔
171
    }
172
    public set type(value) {
173
        this._type = value;
30✔
174
        this.resetPaneSizes();
30✔
175
        this.panes?.notifyOnChanges();
30✔
176
    }
177

178
    /**
179
     * Sets the visibility of the handle and expanders in the splitter bar.
180
     * False by default
181
     *
182
     * @example
183
     * ```html
184
     * <igx-splitter [nonCollapsible]='true'>
185
     * </igx-splitter>
186
     * ```
187
     */
188
    @Input({ transform: booleanAttribute })
189
    public nonCollapsible = false; // Input to toggle showing/hiding expanders
25✔
190

191
    /**
192
     * @hidden @internal
193
     * Gets the `flex-direction` property of the current `SplitterComponent`.
194
     */
195
    @HostBinding('style.flex-direction')
196
    public get direction(): string {
197
        return this.type === SplitterType.Horizontal ? 'row' : 'column';
175✔
198
    }
199

200
    /** @hidden @internal */
201
    public ngAfterContentInit(): void {
202
        this.initPanes();
25✔
203
        this.panes.changes.subscribe(() => {
25✔
204
            this.initPanes();
9✔
205
        });
206
    }
207

208
    /**
209
     * @hidden @internal
210
     * This method performs  initialization logic when the user starts dragging the splitter bar between each pair of panes.
211
     * @param pane - the main pane associated with the currently dragged bar.
212
     */
213
    public onMoveStart(pane: IgxSplitterPaneComponent) {
214
        const panes = this.panes.toArray();
18✔
215
        this.pane = pane;
18✔
216
        this.sibling = panes[panes.indexOf(this.pane) + 1];
18✔
217

218
        const paneRect = this.pane.element.getBoundingClientRect();
18✔
219
        this.initialPaneSize = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height;
18✔
220

221
        const siblingRect = this.sibling.element.getBoundingClientRect();
18✔
222
        this.initialSiblingSize = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height;
18✔
223
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
18✔
224
        this.resizeStart.emit(args);
18✔
225
    }
226

227
    /**
228
     * @hidden @internal
229
     * This method performs calculations concerning the sizes of each pair of panes when the bar between them is dragged.
230
     * @param delta - The difference along the X (or Y) axis between the initial and the current point when dragging the bar.
231
     */
232
    public onMoving(delta: number) {
233
        const [ paneSize, siblingSize ] = this.calcNewSizes(delta);
18✔
234

235
        this.pane.dragSize = paneSize + 'px';
18✔
236
        this.sibling.dragSize = siblingSize + 'px';
18✔
237

238
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
18✔
239
        this.resizing.emit(args);
18✔
240
    }
241

242
    public onMoveEnd(delta: number) {
243
        let [ paneSize, siblingSize ] = this.calcNewSizes(delta);
6✔
244

245
        if (paneSize + siblingSize > this.getTotalSize() && delta < 0) {
6✔
246
            siblingSize = this.getTotalSize() - paneSize;
1✔
247
        } else if (paneSize + siblingSize > this.getTotalSize() && delta > 0) {
5!
UNCOV
248
            paneSize = this.getTotalSize() - siblingSize;
×
249
        }
250

251
        if (this.pane.isPercentageSize) {
6✔
252
            // handle % resizes
253
            const totalSize = this.getTotalSize();
5✔
254
            const percentPaneSize = (paneSize / totalSize) * 100;
5✔
255
            this.pane.size = percentPaneSize + '%';
5✔
256
        } else {
257
            // px resize
258
            this.pane.size = paneSize + 'px';
1✔
259
        }
260

261
        if (this.sibling.isPercentageSize) {
6✔
262
            // handle % resizes
263
            const totalSize = this.getTotalSize();
4✔
264
            const percentSiblingPaneSize = (siblingSize / totalSize) * 100;
4✔
265
            this.sibling.size = percentSiblingPaneSize + '%';
4✔
266
        } else {
267
            // px resize
268
            this.sibling.size = siblingSize + 'px';
2✔
269
        }
270
        this.pane.dragSize = null;
6✔
271
        this.sibling.dragSize = null;
6✔
272

273
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
6✔
274
        this.resizeEnd.emit(args);
6✔
275
    }
276

277
    /** @hidden @internal */
278
    public getPaneSiblingsByOrder(order: number, barIndex: number): Array<IgxSplitterPaneComponent> {
279
        const panes = this.panes.toArray();
224✔
280
        const prevPane = panes[order - barIndex - 1];
224✔
281
        const nextPane = panes[order - barIndex];
224✔
282
        const siblings = [prevPane, nextPane];
224✔
283
        return siblings;
224✔
284
    }
285

286
    private getTotalSize() {
287
        const computed = this.document.defaultView.getComputedStyle(this.elementRef.nativeElement);
22✔
288
        const totalSize = this.type === SplitterType.Horizontal ? computed.getPropertyValue('width') : computed.getPropertyValue('height');
22✔
289
        return parseFloat(totalSize);
22✔
290
    }
291

292

293
    /**
294
     * @hidden @internal
295
     * This method inits panes with properties.
296
     */
297
    private initPanes() {
298
        this.panes.forEach(pane => {
34✔
299
            pane.owner = this;
76✔
300
            if (this.type === SplitterType.Horizontal) {
76✔
301
                pane.minWidth = pane.minSize ?? '0';
57✔
302
                pane.maxWidth = pane.maxSize ?? '100%';
57✔
303
            } else {
304
                pane.minHeight = pane.minSize ?? '0';
19✔
305
                pane.maxHeight = pane.maxSize ?? '100%';
19✔
306
            }
307
        });
308
        this.assignFlexOrder();
34✔
309
        if (this.panes.filter(x => x.collapsed).length > 0) {
76✔
310
            // if any panes are collapsed, reset sizes.
311
            this.resetPaneSizes();
2✔
312
        }
313
    }
314

315
    /**
316
     * @hidden @internal
317
     * This method reset pane sizes.
318
     */
319
    private resetPaneSizes() {
320
        if (this.panes) {
32✔
321
            // if type is changed runtime, should reset sizes.
322
            this.panes.forEach(x => {
9✔
323
                x.size = 'auto'
21✔
324
                x.minWidth = '0';
21✔
325
                x.maxWidth = '100%';
21✔
326
                x.minHeight = '0';
21✔
327
                x.maxHeight = '100%';
21✔
328
            });
329
        }
330
    }
331

332
    /**
333
     * @hidden @internal
334
     * This method assigns the order of each pane.
335
     */
336
    private assignFlexOrder() {
337
        let k = 0;
34✔
338
        this.panes.forEach((pane: IgxSplitterPaneComponent) => {
34✔
339
            pane.order = k;
76✔
340
            k += 2;
76✔
341
        });
342
    }
343

344
    /**
345
     * @hidden @internal
346
     * Calculates new sizes for the panes based on move delta and initial sizes
347
     */
348
    private calcNewSizes(delta: number): [number, number] {
349
        const min = parseInt(this.pane.minSize, 10) || 0;
24✔
350
        const minSibling = parseInt(this.sibling.minSize, 10) || 0;
24✔
351
        const max = parseInt(this.pane.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize - minSibling;
24✔
352
        const maxSibling = parseInt(this.sibling.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize - min;
24✔
353

354
        if (delta < 0) {
24✔
355
            const maxPossibleDelta = Math.min(
16✔
356
                max - this.initialPaneSize,
357
                this.initialSiblingSize - minSibling,
358
            )
359
            delta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1;
16✔
360
        } else {
361
            const maxPossibleDelta = Math.min(
8✔
362
                this.initialPaneSize - min,
363
                maxSibling - this.initialSiblingSize
364
            )
365
            delta = Math.min(maxPossibleDelta, Math.abs(delta));
8✔
366
        }
367
        return [this.initialPaneSize - delta, this.initialSiblingSize + delta];
24✔
368
    }
369
}
370

371
/**
372
 * @hidden @internal
373
 * Represents the draggable bar that visually separates panes and allows for changing their sizes.
374
 */
375
@Component({
376
    selector: 'igx-splitter-bar',
377
    templateUrl: './splitter-bar.component.html',
378
    changeDetection: ChangeDetectionStrategy.Eager,
379
    imports: [IgxDragDirective, IgxDragIgnoreDirective]
380
})
381
export class IgxSplitBarComponent {
3✔
382
    /**
383
     * Set css class to the host element.
384
     */
385
    @HostBinding('class.igx-splitter-bar-host')
386
    public cssClass = 'igx-splitter-bar-host';
32✔
387

388
     /**
389
     * Sets the visibility of the handle and expanders in the splitter bar.
390
     */
391
    @Input({ transform: booleanAttribute })
392
    public nonCollapsible;
393

394
    /**
395
     * Gets/Sets the orientation.
396
     */
397
    @Input()
398
    public type: SplitterType = SplitterType.Horizontal;
32✔
399

400
    /**
401
     * Sets/gets the element order.
402
     */
403
    @HostBinding('style.order')
404
    @Input()
405
    public order!: number;
406

407
    /**
408
     * @hidden
409
     * @internal
410
     */
411
    @HostBinding('attr.tabindex')
412
    public get tabindex() {
413
        return this.resizeDisallowed ? null : 0;
224✔
414
    }
415

416
    /**
417
     * @hidden
418
     * @internal
419
     */
420
    @HostBinding('attr.aria-orientation')
421
    public get orientation() {
422
        return this.type === SplitterType.Horizontal ? 'horizontal' : 'vertical';
224✔
423
    }
424

425
    /**
426
     * @hidden
427
     * @internal
428
     */
429
    public get cursor() {
430
        if (this.resizeDisallowed) {
226✔
431
            return '';
39✔
432
        }
433
        return this.type === SplitterType.Horizontal ? 'col-resize' : 'row-resize';
187✔
434
    }
435

436
    /**
437
     * Sets/gets the `SplitPaneComponent` associated with the current `SplitBarComponent`.
438
     *
439
     * @memberof SplitBarComponent
440
     */
441
    @Input()
442
    public pane!: IgxSplitterPaneComponent;
443

444
    /**
445
     * Sets/Gets the `SplitPaneComponent` sibling components associated with the current `SplitBarComponent`.
446
     */
447
    @Input()
448
    public siblings!: Array<IgxSplitterPaneComponent>;
449

450
    /**
451
     * An event that is emitted whenever we start dragging the current `SplitBarComponent`.
452
     */
453
    @Output()
454
    public moveStart = new EventEmitter<IgxSplitterPaneComponent>();
32✔
455

456
    /**
457
     * An event that is emitted while we are dragging the current `SplitBarComponent`.
458
     */
459
    @Output()
460
    public moving = new EventEmitter<number>();
32✔
461

462
    @Output()
463
    public movingEnd = new EventEmitter<number>();
32✔
464

465
    /**
466
     * A temporary holder for the pointer coordinates.
467
     */
468
    private startPoint!: number;
469

470
    private interactionKeys = new Set('right down left up arrowright arrowdown arrowleft arrowup'.split(' '));
32✔
471

472
    /**
473
     * @hidden @internal
474
     */
475
    public get prevButtonHidden() {
476
        return this.siblings[0]?.collapsed && !this.siblings[1]?.collapsed;
224✔
477
    }
478

479
    /**
480
     * @hidden @internal
481
     */
482
    @HostListener('keydown', ['$event'])
483
    public keyEvent(event: KeyboardEvent) {
484
        const key = event.key.toLowerCase();
17✔
485
        const ctrl = event.ctrlKey;
17✔
486
        event.stopPropagation();
17✔
487
        if (this.interactionKeys.has(key)) {
17✔
488
            event.preventDefault();
17✔
489
        }
490
        switch (key) {
17!
491
            case 'arrowup':
492
            case 'up':
493
                if (this.type === SplitterType.Vertical) {
3✔
494
                    if (ctrl) {
3✔
495
                        this.onCollapsing(false);
2✔
496
                        break;
2✔
497
                    }
498
                    if (!this.resizeDisallowed) {
1✔
499
                        event.preventDefault();
1✔
500
                        this.moveStart.emit(this.pane);
1✔
501
                        this.moving.emit(10);
1✔
502
                    }
503
                }
504
                break;
1✔
505
            case 'arrowdown':
506
            case 'down':
507
                if (this.type === SplitterType.Vertical) {
5✔
508
                    if (ctrl) {
5✔
509
                        this.onCollapsing(true);
2✔
510
                        break;
2✔
511
                    }
512
                    if (!this.resizeDisallowed) {
3✔
513
                        event.preventDefault();
2✔
514
                        this.moveStart.emit(this.pane);
2✔
515
                        this.moving.emit(-10);
2✔
516
                    }
517
                }
518
                break;
3✔
519
            case 'arrowleft':
520
            case 'left':
521
                if (this.type === SplitterType.Horizontal) {
3✔
522
                    if (ctrl) {
3✔
523
                        this.onCollapsing(false);
2✔
524
                        break;
2✔
525
                    }
526
                    if (!this.resizeDisallowed) {
1✔
527
                        event.preventDefault();
1✔
528
                        this.moveStart.emit(this.pane);
1✔
529
                        this.moving.emit(10);
1✔
530
                    }
531
                }
532
                break;
1✔
533
            case 'arrowright':
534
            case 'right':
535
                if (this.type === SplitterType.Horizontal) {
6✔
536
                    if (ctrl) {
6✔
537
                        this.onCollapsing(true);
2✔
538
                        break;
2✔
539
                    }
540
                    if (!this.resizeDisallowed) {
4✔
541
                        event.preventDefault();
2✔
542
                        this.moveStart.emit(this.pane);
2✔
543
                        this.moving.emit(-10);
2✔
544
                    }
545
                }
546
                break;
4✔
547
            default:
UNCOV
548
                break;
×
549
        }
550
    }
551

552
    /**
553
     * @hidden @internal
554
     */
555
    public get dragDir() {
556
        return this.type === SplitterType.Horizontal ? DragDirection.VERTICAL : DragDirection.HORIZONTAL;
224✔
557
    }
558

559
    /**
560
     * @hidden @internal
561
     */
562
    public get nextButtonHidden() {
563
        return this.siblings[1]?.collapsed && !this.siblings[0]?.collapsed;
224✔
564
    }
565

566
    /**
567
     * @hidden @internal
568
     */
569
    public onDragStart(event: IDragStartEventArgs) {
570
        if (this.resizeDisallowed) {
1✔
571
            event.cancel = true;
1✔
572
            return;
1✔
573
        }
UNCOV
574
        this.startPoint = this.type === SplitterType.Horizontal ? event.startX : event.startY;
×
UNCOV
575
        this.moveStart.emit(this.pane);
×
576
    }
577

578
    /**
579
     * @hidden @internal
580
     */
581
    public onDragMove(event: IDragMoveEventArgs) {
UNCOV
582
        const isHorizontal = this.type === SplitterType.Horizontal;
×
UNCOV
583
        const curr = isHorizontal ? event.pageX : event.pageY;
×
UNCOV
584
        const delta = this.startPoint - curr;
×
UNCOV
585
        if (delta !== 0) {
×
586
            this.moving.emit(delta);
×
587
            event.cancel = true;
×
588
            event.owner.element.nativeElement.style.transform = '';
×
589
        }
590
    }
591

592
    public onDragEnd(event: any) {
UNCOV
593
        const isHorizontal = this.type === SplitterType.Horizontal;
×
UNCOV
594
        const curr = isHorizontal ? event.pageX : event.pageY;
×
UNCOV
595
        const delta = this.startPoint - curr;
×
UNCOV
596
        if (delta !== 0) {
×
597
            this.movingEnd.emit(delta);
×
598
        }
599
    }
600

601
    protected get resizeDisallowed() {
602
        const relatedTabs = this.siblings;
460✔
603
        return !!relatedTabs.find(x => x?.resizable === false || x?.collapsed === true);
872✔
604
    }
605

606
    /**
607
     * @hidden @internal
608
     */
609
    public onCollapsing(next: boolean) {
610
        const prevSibling = this.siblings[0];
15✔
611
        const nextSibling = this.siblings[1];
15✔
612
        let target;
613
        if (next) {
15✔
614
            // if next is clicked when prev pane is hidden, show prev pane, else hide next pane.
615
            target = prevSibling.collapsed ? prevSibling : nextSibling;
7✔
616
        } else {
617
            // if prev is clicked when next pane is hidden, show next pane, else hide prev pane.
618
            target = nextSibling.collapsed ? nextSibling : prevSibling;
8✔
619
        }
620
        target.toggle();
15✔
621
    }
622
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc