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

IgniteUI / igniteui-angular / 15927619473

27 Jun 2025 01:35PM UTC coverage: 91.49% (+0.08%) from 91.411%
15927619473

Pull #15982

github

web-flow
Merge c3e682423 into 500cbe870
Pull Request #15982: Address grids column headers accessibility issues - active descendant, what is announced by SRs

13422 of 15738 branches covered (85.28%)

30 of 31 new or added lines in 7 files covered. (96.77%)

11 existing lines in 1 file now uncovered.

27112 of 29634 relevant lines covered (91.49%)

39325.58 hits per line

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

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

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

14
export declare interface ISplitterBarResizeEventArgs {
15
    pane: IgxSplitterPaneComponent;
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>
43
 * ```
44
 */
45
@Component({
46
    selector: 'igx-splitter',
47
    templateUrl: './splitter.component.html',
48
    imports: [forwardRef(() => IgxSplitBarComponent)]
119✔
49
})
50
export class IgxSplitterComponent implements AfterContentInit {
3✔
51
    /**
52
     * Gets the list of splitter panes.
53
     *
54
     * @example
55
     * ```typescript
56
     * const panes = this.splitter.panes;
57
     * ```
58
     */
59
    @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent })
60
    public panes!: QueryList<IgxSplitterPaneComponent>;
61

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

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

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

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

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

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

118

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

132
    private _type: SplitterType = SplitterType.Horizontal;
24✔
133

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

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

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

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

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

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

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

199
    /** @hidden @internal */
200
    public ngAfterContentInit(): void {
201
        this.zone.onStable.pipe(take(1)).subscribe(() => {
24✔
202
            this.initPanes();
24✔
203
        });
204
        this.panes.changes.subscribe(() => {
24✔
205
            this.initPanes();
9✔
206
        });
207
    }
208

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

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

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

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

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

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

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

246
        if (paneSize + siblingSize > this.getTotalSize() && delta < 0) {
6!
247
            paneSize = this.getTotalSize();
×
248
            siblingSize = 0;
×
249
        } else if(paneSize + siblingSize > this.getTotalSize() && delta > 0) {
6!
UNCOV
250
            paneSize = 0;
×
UNCOV
251
            siblingSize = this.getTotalSize();
×
252
        }
253

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

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

276
        const args: ISplitterBarResizeEventArgs = { pane: this.pane, sibling: this.sibling };
6✔
277
        this.resizeEnd.emit(args);
6✔
278
    }
279

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

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

295

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

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

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

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

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

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

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

396
    /**
397
     * Gets/Sets the orientation.
398
     */
399
    @Input()
400
    public type: SplitterType = SplitterType.Horizontal;
31✔
401

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

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

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

427
    /**
428
     * @hidden
429
     * @internal
430
     */
431
    public get cursor() {
432
        if (this.resizeDisallowed) {
224✔
433
            return '';
35✔
434
        }
435
        return this.type === SplitterType.Horizontal ? 'col-resize' : 'row-resize';
189✔
436
    }
437

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

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

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

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

464
    @Output()
465
    public movingEnd = new EventEmitter<number>();
31✔
466

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

472
    private interactionKeys = new Set('right down left up arrowright arrowdown arrowleft arrowup'.split(' '));
31✔
473

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

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

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

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

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

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

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

603
    protected get resizeDisallowed() {
604
        const relatedTabs = this.siblings;
456✔
605
        return !!relatedTabs.find(x => x?.resizable === false || x?.collapsed === true);
864✔
606
    }
607

608
    /**
609
     * @hidden @internal
610
     */
611
    public onCollapsing(next: boolean) {
612
        const prevSibling = this.siblings[0];
15✔
613
        const nextSibling = this.siblings[1];
15✔
614
        let target;
615
        if (next) {
15✔
616
            // if next is clicked when prev pane is hidden, show prev pane, else hide next pane.
617
            target = prevSibling.collapsed ? prevSibling : nextSibling;
7✔
618
        } else {
619
            // if prev is clicked when next pane is hidden, show next pane, else hide prev pane.
620
            target = nextSibling.collapsed ? nextSibling : prevSibling;
8✔
621
        }
622
        target.toggle();
15✔
623
    }
624
}
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