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

IgniteUI / igniteui-angular / 5817963878

pending completion
5817963878

push

github

web-flow
fix(splitter): improving sizing calculations in percent splitter #13252 - 16.0 (#13357)

15302 of 17979 branches covered (85.11%)

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

26832 of 29083 relevant lines covered (92.26%)

29650.86 hits per line

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

87.5
/projects/igniteui-angular/src/lib/splitter/splitter.component.ts
1
import { DOCUMENT, NgFor, NgIf } from '@angular/common';
2
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Input, Output, QueryList, forwardRef } from '@angular/core';
3
import { DragDirection, IDragMoveEventArgs, IDragStartEventArgs, IgxDragDirective, IgxDragIgnoreDirective } from '../directives/drag-drop/drag-drop.directive';
4
import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component';
5

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

2✔
14
export declare interface ISplitterBarResizeEventArgs {
2✔
15
    pane: IgxSplitterPaneComponent;
4✔
16
    sibling: IgxSplitterPaneComponent;
17
}
18

19
/**
20
 * Provides a framework for a simple layout, splitting the view horizontally or vertically
21
 * into multiple smaller resizable and collapsible areas.
22
 *
23
 * @igxModule IgxSplitterModule
24
 *
25
 * @igxParent Layouts
26
 *
27
 * @igxTheme igx-splitter-theme
28
 *
29
 * @igxKeywords splitter panes layout
30
 *
31
 * @igxGroup presentation
32
 *
33
 * @example
34
 * ```html
35
 * <igx-splitter>
36
 *  <igx-splitter-pane>
37
 *      ...
38
 *  </igx-splitter-pane>
39
 *  <igx-splitter-pane>
40
 *      ...
41
 *  </igx-splitter-pane>
42
 * </igx-splitter>
2✔
43
 * ```
44
 */
132✔
45
@Component({
46
    selector: 'igx-splitter',
47
    templateUrl: './splitter.component.html',
19✔
48
    standalone: true,
19✔
49
    imports: [NgFor, NgIf, forwardRef(() => IgxSplitBarComponent)]
19✔
50
})
19✔
51
export class IgxSplitterComponent implements AfterContentInit {
19✔
52
    /**
19✔
53
     * Gets the list of splitter panes.
19✔
54
     *
19✔
55
     * @example
19✔
56
     * ```typescript
57
     * const panes = this.splitter.panes;
58
     * ```
530✔
59
     */
60
    @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent })
61
    public panes!: QueryList<IgxSplitterPaneComponent>;
25✔
62

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

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

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

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

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

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

3✔
119

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

170✔
133
    private _type: SplitterType = SplitterType.Horizontal;
170✔
134

170✔
135
    /**
170✔
136
     * @hidden @internal
170✔
137
     * A field that holds the initial size of the main `IgxSplitterPaneComponent` in each pair of panes divided by a splitter bar.
138
     */
139
    private initialPaneSize!: number;
5✔
140

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

148
    /**
27✔
149
     * @hidden @internal
61✔
150
     * The main pane in each pair of panes divided by a gripper.
61✔
151
     */
44✔
152
    private pane!: IgxSplitterPaneComponent;
44✔
153

154
    /**
155
     * The sibling pane in each pair of panes divided by a splitter bar.
17✔
156
     */
17✔
157
    private sibling!: IgxSplitterPaneComponent;
158

159
    constructor(@Inject(DOCUMENT) public document, private elementRef: ElementRef) { }
27✔
160
    /**
61✔
161
     * Gets/Sets the splitter orientation.
162
     *
2✔
163
     * @example
164
     * ```html
165
     * <igx-splitter [type]="type">...</igx-splitter>
166
     * ```
167
     */
168
    @Input()
169
    public get type() {
170
        return this._type;
27✔
171
    }
172
    public set type(value) {
8✔
173
        this._type = value;
19✔
174
        this.resetPaneSizes();
19✔
175
        this.panes?.notifyOnChanges();
19✔
176
    }
19✔
177

19✔
178
    /**
179
     * @hidden @internal
180
     * Gets the `flex-direction` property of the current `SplitterComponent`.
181
     */
182
    @HostBinding('style.flex-direction')
183
    public get direction(): string {
184
        return this.type === SplitterType.Horizontal ? 'row' : 'column';
185
    }
186

27✔
187
    /** @hidden @internal */
27✔
188
    public ngAfterContentInit(): void {
61✔
189
        this.initPanes();
61✔
190
        this.panes.changes.subscribe(() => {
191
            this.initPanes();
192
        });
193
    }
194

195
    /**
196
     * @hidden @internal
197
     * This method performs  initialization logic when the user starts dragging the splitter bar between each pair of panes.
20✔
198
     * @param pane - the main pane associated with the currently dragged bar.
20✔
199
     */
20✔
200
    public onMoveStart(pane: IgxSplitterPaneComponent) {
20✔
201
        const panes = this.panes.toArray();
20✔
202
        this.pane = pane;
14✔
203
        this.sibling = panes[panes.indexOf(this.pane) + 1];
14✔
204

205
        const paneRect = this.pane.element.getBoundingClientRect();
206
        this.initialPaneSize = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height;
6✔
207

6✔
208
        const siblingRect = this.sibling.element.getBoundingClientRect();
209
        this.initialSiblingSize = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height;
20✔
210
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
211
        this.resizeStart.emit(args);
2✔
212
    }
213

214
    /**
215
     * @hidden @internal
2✔
216
     * This method performs calculations concerning the sizes of each pair of panes when the bar between them is dragged.
217
     * @param delta - The difference along the X (or Y) axis between the initial and the current point when dragging the bar.
218
     */
219
    public onMoving(delta: number) {
220
        const [ paneSize, siblingSize ] = this.calcNewSizes(delta);
221

222
        this.pane.dragSize = paneSize + 'px';
223
        this.sibling.dragSize = siblingSize + 'px';
224

225
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
226
        this.resizing.emit(args);
227
    }
228

2✔
229
    public onMoveEnd(delta: number) {
230
        const [ paneSize, siblingSize ] = this.calcNewSizes(delta);
231

232
        if (this.pane.isPercentageSize) {
233
            // handle % resizes
47✔
234
            const totalSize = this.getTotalSize();
235
            const percentPaneSize = (paneSize / totalSize) * 100;
236
            this.pane.size = percentPaneSize + '%';
237
        } else {
2✔
238
            // px resize
239
            this.pane.size = paneSize + 'px';
240
        }
241

242
        if (this.sibling.isPercentageSize) {
2✔
243
            // handle % resizes
244
            const totalSize = this.getTotalSize();
25✔
245
            const percentSiblingPaneSize = (siblingSize / totalSize) * 100;
25✔
246
            this.sibling.size = percentSiblingPaneSize + '%';
25✔
247
        } else {
25✔
248
            // px resize
25✔
249
            this.sibling.size = siblingSize + 'px';
250
        }
251
        this.pane.dragSize = null;
170✔
252
        this.sibling.dragSize = null;
253

254
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
170✔
255
        this.resizeEnd.emit(args);
256
    }
257

258
    /** @hidden @internal */
259
    public getPaneSiblingsByOrder(order: number, barIndex: number): Array<IgxSplitterPaneComponent> {
260
        const panes = this.panes.toArray();
261
        const prevPane = panes[order - barIndex - 1];
172✔
262
        const nextPane = panes[order - barIndex];
39✔
263
        const siblings = [prevPane, nextPane];
264
        return siblings;
133✔
265
    }
266

267
    private getTotalSize() {
268
        const computed = this.document.defaultView.getComputedStyle(this.elementRef.nativeElement);
269
        const totalSize = this.type === SplitterType.Horizontal ? computed.getPropertyValue('width') : computed.getPropertyValue('height');
270
        return parseFloat(totalSize);
170✔
271
    }
272

273

17✔
274
    /**
17✔
275
     * @hidden @internal
17✔
276
     * This method inits panes with properties.
17!
277
     */
17✔
278
    private initPanes() {
279
        this.panes.forEach(pane => {
17!
280
            pane.owner = this;
281
            if (this.type === SplitterType.Horizontal) {
282
                pane.minWidth = pane.minSize ?? '0';
3!
283
                pane.maxWidth = pane.maxSize ?? '100%';
3✔
284
            } else {
2✔
285
                pane.minHeight = pane.minSize ?? '0';
2✔
286
                pane.maxHeight = pane.maxSize ?? '100%';
287
            }
1!
288
        });
1✔
289
        this.assignFlexOrder();
1✔
290
        if (this.panes.filter(x => x.collapsed).length > 0) {
1✔
291
            // if any panes are collapsed, reset sizes.
292
            this.resetPaneSizes();
293
        }
1✔
294
    }
295

296
    /**
5!
297
     * @hidden @internal
5✔
298
     * This method reset pane sizes.
2✔
299
     */
2✔
300
    private resetPaneSizes() {
301
        if (this.panes) {
3✔
302
            // if type is changed runtime, should reset sizes.
2✔
303
            this.panes.forEach(x => {
2✔
304
                x.size = 'auto'
2✔
305
                x.minWidth = '0';
306
                x.maxWidth = '100%';
307
                x.minHeight = '0';
3✔
308
                x.maxHeight = '100%';
309
            });
310
        }
3!
311
    }
3✔
312

2✔
313
    /**
2✔
314
     * @hidden @internal
315
     * This method assigns the order of each pane.
1!
316
     */
1✔
317
    private assignFlexOrder() {
1✔
318
        let k = 0;
1✔
319
        this.panes.forEach((pane: IgxSplitterPaneComponent) => {
320
            pane.order = k;
321
            k += 2;
1✔
322
        });
323
    }
324

6!
325
    /**
6✔
326
     * @hidden @internal
2✔
327
     * Calculates new sizes for the panes based on move delta and initial sizes
2✔
328
     */
329
    private calcNewSizes(delta: number): [number, number] {
4✔
330
        const min = parseInt(this.pane.minSize, 10) || 0;
2✔
331
        const minSibling = parseInt(this.sibling.minSize, 10) || 0;
2✔
332
        const max = parseInt(this.pane.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize - minSibling;
2✔
333
        const maxSibling = parseInt(this.sibling.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize - min;
334

335
        if (delta < 0) {
4✔
336
            const maxPossibleDelta = Math.min(
337
                max - this.initialPaneSize,
×
338
                this.initialSiblingSize - minSibling,
339
            )
340
            delta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1;
341
        } else {
342
            const maxPossibleDelta = Math.min(
343
                this.initialPaneSize - min,
344
                maxSibling - this.initialSiblingSize
170✔
345
            )
346
            delta = Math.min(maxPossibleDelta, Math.abs(delta));
347
        }
348
        return [this.initialPaneSize - delta, this.initialSiblingSize + delta];
349
    }
350
}
170✔
351

352
export const SPLITTER_INTERACTION_KEYS = new Set('right down left up arrowright arrowdown arrowleft arrowup'.split(' '));
353

354
/**
355
 * @hidden @internal
356
 * Represents the draggable bar that visually separates panes and allows for changing their sizes.
1!
357
 */
1✔
358
@Component({
1✔
359
    selector: 'igx-splitter-bar',
360
    templateUrl: './splitter-bar.component.html',
×
361
    standalone: true,
×
362
    imports: [IgxDragDirective, IgxDragIgnoreDirective]
363
})
364
export class IgxSplitBarComponent {
365
    /**
366
     * Set css class to the host element.
367
     */
×
368
    @HostBinding('class.igx-splitter-bar-host')
×
369
    public cssClass = 'igx-splitter-bar-host';
×
370

×
371
    /**
×
372
     * Gets/Sets the orientation.
×
373
     */
×
374
    @Input()
375
    public type: SplitterType = SplitterType.Horizontal;
376

377
    /**
×
378
     * Sets/gets the element order.
×
379
     */
×
380
    @HostBinding('style.order')
×
381
    @Input()
×
382
    public order!: number;
383

384
    /**
385
     * @hidden
352✔
386
     * @internal
656✔
387
     */
388
    @HostBinding('attr.tabindex')
389
    public get tabindex() {
390
        return this.resizeDisallowed ? null : 0;
391
    }
392

15✔
393
    /**
15✔
394
     * @hidden
395
     * @internal
15✔
396
     */
397
    @HostBinding('attr.aria-orientation')
7✔
398
    public get orientation() {
399
        return this.type === SplitterType.Horizontal ? 'horizontal' : 'vertical';
400
    }
401

8✔
402
    /**
403
     * @hidden
15✔
404
     * @internal
405
     */
2✔
406
    public get cursor() {
407
        if (this.resizeDisallowed) {
408
            return '';
409
        }
410
        return this.type === SplitterType.Horizontal ? 'col-resize' : 'row-resize';
411
    }
412

413
    /**
414
     * Sets/gets the `SplitPaneComponent` associated with the current `SplitBarComponent`.
415
     *
416
     * @memberof SplitBarComponent
417
     */
418
    @Input()
419
    public pane!: IgxSplitterPaneComponent;
2✔
420

421
    /**
422
     * Sets/Gets the `SplitPaneComponent` sibling components associated with the current `SplitBarComponent`.
423
     */
424
    @Input()
425
    public siblings!: Array<IgxSplitterPaneComponent>;
426

427
    /**
428
     * An event that is emitted whenever we start dragging the current `SplitBarComponent`.
429
     */
430
    @Output()
431
    public moveStart = new EventEmitter<IgxSplitterPaneComponent>();
432

433
    /**
434
     * An event that is emitted while we are dragging the current `SplitBarComponent`.
435
     */
436
    @Output()
437
    public moving = new EventEmitter<number>();
438

439
    @Output()
440
    public movingEnd = new EventEmitter<number>();
441

442
    /**
443
     * A temporary holder for the pointer coordinates.
444
     */
445
    private startPoint!: number;
446

447
    /**
448
     * @hidden @internal
449
     */
450
    public get prevButtonHidden() {
451
        return this.siblings[0].collapsed && !this.siblings[1].collapsed;
452
    }
453

454
    /**
455
     * @hidden @internal
456
     */
457
    @HostListener('keydown', ['$event'])
458
    public keyEvent(event: KeyboardEvent) {
459
        const key = event.key.toLowerCase();
460
        const ctrl = event.ctrlKey;
461
        event.stopPropagation();
462
        if (SPLITTER_INTERACTION_KEYS.has(key)) {
463
            event.preventDefault();
464
        }
465
        switch (key) {
466
            case 'arrowup':
467
            case 'up':
468
                if (this.type === SplitterType.Vertical) {
469
                    if (ctrl) {
470
                        this.onCollapsing(false);
471
                        break;
472
                    }
473
                    if (!this.resizeDisallowed) {
474
                        event.preventDefault();
475
                        this.moveStart.emit(this.pane);
476
                        this.moving.emit(10);
477
                    }
478
                }
479
                break;
480
            case 'arrowdown':
481
            case 'down':
482
                if (this.type === SplitterType.Vertical) {
483
                    if (ctrl) {
484
                        this.onCollapsing(true);
485
                        break;
486
                    }
487
                    if (!this.resizeDisallowed) {
488
                        event.preventDefault();
489
                        this.moveStart.emit(this.pane);
490
                        this.moving.emit(-10);
491
                    }
492
                }
493
                break;
494
            case 'arrowleft':
495
            case 'left':
496
                if (this.type === SplitterType.Horizontal) {
497
                    if (ctrl) {
498
                        this.onCollapsing(false);
499
                        break;
500
                    }
501
                    if (!this.resizeDisallowed) {
502
                        event.preventDefault();
503
                        this.moveStart.emit(this.pane);
504
                        this.moving.emit(10);
505
                    }
506
                }
507
                break;
508
            case 'arrowright':
509
            case 'right':
510
                if (this.type === SplitterType.Horizontal) {
511
                    if (ctrl) {
512
                        this.onCollapsing(true);
513
                        break;
514
                    }
515
                    if (!this.resizeDisallowed) {
516
                        event.preventDefault();
517
                        this.moveStart.emit(this.pane);
518
                        this.moving.emit(-10);
519
                    }
520
                }
521
                break;
522
            default:
523
                break;
524
        }
525
    }
526

527
    /**
528
     * @hidden @internal
529
     */
530
    public get dragDir() {
531
        return this.type === SplitterType.Horizontal ? DragDirection.VERTICAL : DragDirection.HORIZONTAL;
532
    }
533

534
    /**
535
     * @hidden @internal
536
     */
537
    public get nextButtonHidden() {
538
        return this.siblings[1].collapsed && !this.siblings[0].collapsed;
539
    }
540

541
    /**
542
     * @hidden @internal
543
     */
544
    public onDragStart(event: IDragStartEventArgs) {
545
        if (this.resizeDisallowed) {
546
            event.cancel = true;
547
            return;
548
        }
549
        this.startPoint = this.type === SplitterType.Horizontal ? event.startX : event.startY;
550
        this.moveStart.emit(this.pane);
551
    }
552

553
    /**
554
     * @hidden @internal
555
     */
556
    public onDragMove(event: IDragMoveEventArgs) {
557
        const isHorizontal = this.type === SplitterType.Horizontal;
558
        const curr = isHorizontal ? event.pageX : event.pageY;
559
        const delta = this.startPoint - curr;
560
        if (delta !== 0) {
561
            this.moving.emit(delta);
562
            event.cancel = true;
563
            event.owner.element.nativeElement.style.transform = '';
564
        }
565
    }
566

567
    public onDragEnd(event: any) {
568
        const isHorizontal = this.type === SplitterType.Horizontal;
569
        const curr = isHorizontal ? event.pageX : event.pageY;
570
        const delta = this.startPoint - curr;
571
        if (delta !== 0) {
572
            this.movingEnd.emit(delta);
573
        }
574
    }
575

576
    protected get resizeDisallowed() {
577
        const relatedTabs = this.siblings;
578
        return !!relatedTabs.find(x => x.resizable === false || x.collapsed === true);
579
    }
580

581
    /**
582
     * @hidden @internal
583
     */
584
    public onCollapsing(next: boolean) {
585
        const prevSibling = this.siblings[0];
586
        const nextSibling = this.siblings[1];
587
        let target;
588
        if (next) {
589
            // if next is clicked when prev pane is hidden, show prev pane, else hide next pane.
590
            target = prevSibling.collapsed ? prevSibling : nextSibling;
591
        } else {
592
            // if prev is clicked when next pane is hidden, show next pane, else hide prev pane.
593
            target = nextSibling.collapsed ? nextSibling : prevSibling;
594
        }
595
        target.toggle();
596
    }
597
}
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