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

IgniteUI / igniteui-angular / 5817971405

pending completion
5817971405

push

github

web-flow
fix(splitter): improving sizing calculations in percent splitter #13252 - 15.1 (#13356)

15307 of 17976 branches covered (85.15%)

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

26834 of 29076 relevant lines covered (92.29%)

29767.64 hits per line

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

87.46
/projects/igniteui-angular/src/lib/splitter/splitter.component.ts
1
import { DOCUMENT } from '@angular/common';
2
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Input, Output, QueryList } from '@angular/core';
3
import { DragDirection, IDragMoveEventArgs, IDragStartEventArgs } 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
})
19✔
49
export class IgxSplitterComponent implements AfterContentInit {
19✔
50
    /**
19✔
51
     * Gets the list of splitter panes.
19✔
52
     *
19✔
53
     * @example
19✔
54
     * ```typescript
19✔
55
     * const panes = this.splitter.panes;
19✔
56
     * ```
57
     */
58
    @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent })
530✔
59
    public panes!: QueryList<IgxSplitterPaneComponent>;
60

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

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

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

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

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

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

117

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

131
    private _type: SplitterType = SplitterType.Horizontal;
132

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

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

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

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

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

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

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

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

14✔
203
        const paneRect = this.pane.element.getBoundingClientRect();
14✔
204
        this.initialPaneSize = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height;
205

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

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

220
        this.pane.dragSize = paneSize + 'px';
221
        this.sibling.dragSize = siblingSize + 'px';
222

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

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

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

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

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

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

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

271

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

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

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

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

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

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

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

×
367
    /**
×
368
     * Gets/Sets the orientation.
×
369
     */
×
370
    @Input()
×
371
    public type: SplitterType = SplitterType.Horizontal;
×
372

373
    /**
374
     * Sets/gets the element order.
375
     */
×
376
    @HostBinding('style.order')
×
377
    @Input()
×
378
    public order!: number;
×
379

×
380
    /**
381
     * @hidden
382
     * @internal
383
     */
352✔
384
    @HostBinding('attr.tabindex')
656✔
385
    public get tabindex() {
386
        return this.resizeDisallowed ? null : 0;
387
    }
388

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

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

409
    /**
410
     * Sets/gets the `SplitPaneComponent` associated with the current `SplitBarComponent`.
411
     *
412
     * @memberof SplitBarComponent
413
     */
414
    @Input()
415
    public pane!: IgxSplitterPaneComponent;
416

417
    /**
2✔
418
     * Sets/Gets the `SplitPaneComponent` sibling components associated with the current `SplitBarComponent`.
419
     */
420
    @Input()
421
    public siblings!: Array<IgxSplitterPaneComponent>;
422

423
    /**
424
     * An event that is emitted whenever we start dragging the current `SplitBarComponent`.
425
     */
426
    @Output()
427
    public moveStart = new EventEmitter<IgxSplitterPaneComponent>();
428

429
    /**
430
     * An event that is emitted while we are dragging the current `SplitBarComponent`.
431
     */
432
    @Output()
433
    public moving = new EventEmitter<number>();
434

435
    @Output()
436
    public movingEnd = new EventEmitter<number>();
437

438
    /**
439
     * A temporary holder for the pointer coordinates.
440
     */
441
    private startPoint!: number;
442

443
    /**
444
     * @hidden @internal
445
     */
446
    public get prevButtonHidden() {
447
        return this.siblings[0].collapsed && !this.siblings[1].collapsed;
448
    }
449

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

523
    /**
524
     * @hidden @internal
525
     */
526
    public get dragDir() {
527
        return this.type === SplitterType.Horizontal ? DragDirection.VERTICAL : DragDirection.HORIZONTAL;
528
    }
529

530
    /**
531
     * @hidden @internal
532
     */
533
    public get nextButtonHidden() {
534
        return this.siblings[1].collapsed && !this.siblings[0].collapsed;
535
    }
536

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

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

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

572
    protected get resizeDisallowed() {
573
        const relatedTabs = this.siblings;
574
        return !!relatedTabs.find(x => x.resizable === false || x.collapsed === true);
575
    }
576

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