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

IgniteUI / igniteui-angular / 29587398373

17 Jul 2026 02:17PM UTC coverage: 90.116% (-0.02%) from 90.135%
29587398373

Pull #15125

github

web-flow
Merge ac93ee972 into 8ebabbb38
Pull Request #15125: refactor(*): bundle styles with components

14920 of 17397 branches covered (85.76%)

Branch coverage included in aggregate %.

30030 of 32483 relevant lines covered (92.45%)

34494.72 hits per line

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

84.48
/projects/igniteui-angular/button-group/src/button-group/button-group.component.ts
1
import {
2
    AfterViewInit,
3
    Component,
4
    ContentChildren,
5
    ChangeDetectorRef,
6
    EventEmitter,
7
    HostBinding,
8
    Input,
9
    Output,
10
    QueryList,
11
    Renderer2,
12
    ViewChildren,
13
    OnDestroy,
14
    ElementRef,
15
    booleanAttribute,
16
    inject,
17
    ChangeDetectionStrategy,
18
    ViewEncapsulation,
19
} from '@angular/core';
20
import { Subject } from 'rxjs';
21
import { IgxButtonDirective } from 'igniteui-angular/directives';
22
import { IgxRippleDirective } from 'igniteui-angular/directives';
23

24
import { takeUntil } from 'rxjs/operators';
25
import { IBaseEventArgs } from 'igniteui-angular/core';
26
import { IgxIconComponent } from 'igniteui-angular/icon';
27

28
/**
29
 * Determines the Button Group alignment
30
 */
31
export const ButtonGroupAlignment = {
3✔
32
    horizontal: 'horizontal',
33
    vertical: 'vertical'
34
} as const;
35
export type ButtonGroupAlignment = typeof ButtonGroupAlignment[keyof typeof ButtonGroupAlignment];
36

37
let NEXT_ID = 0;
3✔
38

39
/**
40
 * **Ignite UI for Angular Button Group** -
41
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html)
42
 *
43
 * The Ignite UI Button Group displays a group of buttons either vertically or horizontally. The group supports
44
 * single, multi and singleRequired selection.
45
 *
46
 * Example:
47
 * ```html
48
 * <igx-buttongroup selectionMode="multi" [values]="fontOptions">
49
 * </igx-buttongroup>
50
 * ```
51
 * The `fontOptions` value shown above is defined as:
52
 * ```typescript
53
 * this.fontOptions = [
54
 *   { icon: 'format_bold', selected: false },
55
 *   { icon: 'format_italic', selected: false },
56
 *   { icon: 'format_underlined', selected: false }];
57
 * ```
58
 */
59
@Component({
60
    selector: 'igx-buttongroup',
61
    templateUrl: 'button-group-content.component.html',
62
    styleUrl: 'buttongroup-content.component.css',
63
    encapsulation: ViewEncapsulation.None,
64
    changeDetection: ChangeDetectionStrategy.Eager,
65
    imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent]
66
})
67
export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
3✔
68
    private _cdr = inject(ChangeDetectorRef);
97✔
69
    private _renderer = inject(Renderer2);
97✔
70
    private _el = inject(ElementRef);
97✔
71

72
    /**
73
     * A collection containing all buttons inside the button group.
74
     */
75
    public get buttons(): IgxButtonDirective[] {
76
        return [...this.viewButtons.toArray(), ...this.templateButtons.toArray()];
947✔
77
    }
78

79
    /**
80
     * Gets/Sets the value of the `id` attribute. If not set it will be automatically generated.
81
     * ```html
82
     *  <igx-buttongroup [id]="'igx-dialog-56'" [selectionMode]="'multi'" [values]="alignOptions">
83
     * ```
84
     */
85
    @HostBinding('attr.id')
86
    @Input()
87
    public id = `igx-buttongroup-${NEXT_ID++}`;
97✔
88

89
    /** @hidden @internal */
90
    @HostBinding('class.igx-button-group')
91
    public cssClass = 'igx-button-group';
97✔
92

93
    /** @hidden @internal */
94

95
    /**
96
     * @hidden
97
     */
98
    @HostBinding('style.zIndex')
99
    public zIndex = 0;
97✔
100

101
    /**
102
     * Sets/gets the role attribute value.
103
     *
104
     * @example
105
     * ```typescript
106
     * @ViewChild("MyButtonGroup", { read: IgxButtonGroupComponent })
107
     * public badge: IgxButtonGroupComponent;
108
     *
109
     * buttonGroup.role = 'group';
110
     * ```
111
     */
112
    @HostBinding('attr.role')
113
    public role = 'group';
97✔
114

115
    /**
116
     * Allows you to set a style using the `itemContentCssClass` input.
117
     * The value should be the CSS class name that will be applied to the button group.
118
     * ```typescript
119
     * public style1 = "styleClass";
120
     *  //..
121
     * ```
122
     *  ```html
123
     * <igx-buttongroup [itemContentCssClass]="style1" [selectionMode]="'multi'" [values]="alignOptions">
124
     * ```
125
     */
126
    @Input()
127
    public set itemContentCssClass(value: string) {
128
        this._itemContentCssClass = value || this._itemContentCssClass;
4!
129
    }
130

131
    /**
132
     * Returns the CSS class of the item content of the button group.
133
     * ```typescript
134
     *  @ViewChild("MyChild")
135
     * public buttonG: IgxButtonGroupComponent;
136
     * ngAfterViewInit(){
137
     *    let buttonSelect = this.buttonG.itemContentCssClass;
138
     * }
139
     * ```
140
     */
141
    public get itemContentCssClass(): string {
142
        return this._itemContentCssClass;
104✔
143
    }
144

145
    /**
146
     * Enables selecting multiple buttons. By default, multi-selection is false.
147
     *
148
     * @deprecated in version 16.1.0. Use the `selectionMode` property instead.
149
     */
150
    @Input()
151
    public get multiSelection() {
152
        if (this.selectionMode === 'multi') {
×
153
            return true;
×
154
        } else {
155
            return false;
×
156
        }
157
    }
158
    public set multiSelection(selectionMode: boolean) {
159
        if (selectionMode) {
×
160
            this.selectionMode = 'multi';
×
161
        } else {
162
            this.selectionMode = 'single';
×
163
        }
164
    }
165

166
    /**
167
     * Gets/Sets the selection mode to 'single', 'singleRequired' or 'multi' of the buttons. By default, the selection mode is 'single'.
168
     * ```html
169
     * <igx-buttongroup [selectionMode]="'multi'" [alignment]="alignment"></igx-buttongroup>
170
     * ```
171
     */
172
    @Input()
173
    public get selectionMode() {
174
        return this._selectionMode;
156✔
175
    }
176
    public set selectionMode(selectionMode: 'single' | 'singleRequired' | 'multi') {
177
        if (this.viewButtons && selectionMode !== this._selectionMode) {
12✔
178
            this.buttons.forEach((_b, i) => {
4✔
179
                this.deselectButton(i);
13✔
180
            });
181
            this._selectionMode = selectionMode;
4✔
182
        } else {
183
            this._selectionMode = selectionMode;
8✔
184
        }
185
    }
186

187
    /**
188
     * Property that configures the buttons in the button group using a collection of `Button` objects.
189
     * ```typescript
190
     *  public ngOnInit() {
191
     *      this.cities = [
192
     *        new Button({
193
     *          label: "Sofia"
194
     *      }),
195
     *        new Button({
196
     *          label: "London"
197
     *      }),
198
     *        new Button({
199
     *          label: "New York",
200
     *          selected: true
201
     *      }),
202
     *        new Button({
203
     *          label: "Tokyo"
204
     *      })
205
     *  ];
206
     *  }
207
     *  //..
208
     * ```
209
     * ```html
210
     *  <igx-buttongroup [selectionMode]="'single'" [values]="cities"></igx-buttongroup>
211
     * ```
212
     */
213
    @Input() public values: any;
214

215
    /**
216
     * Disables the `igx-buttongroup` component. By default it's false.
217
     * ```html
218
     * <igx-buttongroup [disabled]="true" [selectionMode]="'multi'" [values]="fontOptions"></igx-buttongroup>
219
     * ```
220
     */
221
    @Input({ transform: booleanAttribute })
222
    public get disabled(): boolean {
223
        return this._disabled;
326✔
224
    }
225
    public set disabled(value: boolean) {
226
        if (this._disabled !== value) {
×
227
            this._disabled = value;
×
228

229
            if (this.viewButtons && this.templateButtons) {
×
230
                this.buttons.forEach((b) => (b.disabled = this._disabled));
×
231
            }
232
        }
233
    }
234

235
    /**
236
     * Allows you to set the button group alignment.
237
     * Available options are `ButtonGroupAlignment.horizontal` (default) and `ButtonGroupAlignment.vertical`.
238
     * ```typescript
239
     * public alignment = ButtonGroupAlignment.vertical;
240
     * //..
241
     * ```
242
     * ```html
243
     * <igx-buttongroup [selectionMode]="'single'" [values]="cities" [alignment]="alignment"></igx-buttongroup>
244
     * ```
245
     */
246
    @Input()
247
    public set alignment(value: ButtonGroupAlignment) {
248
        this._isVertical = value === ButtonGroupAlignment.vertical;
7✔
249
    }
250
    /**
251
     * Returns the alignment of the `igx-buttongroup`.
252
     * ```typescript
253
     * @ViewChild("MyChild")
254
     * public buttonG: IgxButtonGroupComponent;
255
     * ngAfterViewInit(){
256
     *    let buttonAlignment = this.buttonG.alignment;
257
     * }
258
     * ```
259
     */
260
    public get alignment(): ButtonGroupAlignment {
261
        return this._isVertical ? ButtonGroupAlignment.vertical : ButtonGroupAlignment.horizontal;
2✔
262
    }
263

264
    /**
265
     * An @Ouput property that emits an event when a button is selected.
266
     * ```typescript
267
     * @ViewChild("toast")
268
     * private toast: IgxToastComponent;
269
     * public selectedHandler(buttongroup) {
270
     *     this.toast.open()
271
     * }
272
     *  //...
273
     * ```
274
     * ```html
275
     * <igx-buttongroup #MyChild [selectionMode]="'multi'" (selected)="selectedHandler($event)"></igx-buttongroup>
276
     * <igx-toast #toast>You have made a selection!</igx-toast>
277
     * ```
278
     */
279
    @Output()
280
    public selected = new EventEmitter<IButtonGroupEventArgs>();
97✔
281

282
    /**
283
     * An @Ouput property that emits an event when a button is deselected.
284
     * ```typescript
285
     *  @ViewChild("toast")
286
     *  private toast: IgxToastComponent;
287
     *  public deselectedHandler(buttongroup){
288
     *     this.toast.open()
289
     * }
290
     *  //...
291
     * ```
292
     * ```html
293
     * <igx-buttongroup> #MyChild [selectionMode]="'multi'" (deselected)="deselectedHandler($event)"></igx-buttongroup>
294
     * <igx-toast #toast>You have deselected a button!</igx-toast>
295
     * ```
296
     */
297
    @Output()
298
    public deselected = new EventEmitter<IButtonGroupEventArgs>();
97✔
299

300
    @ViewChildren(IgxButtonDirective) private viewButtons: QueryList<IgxButtonDirective>;
301
    @ContentChildren(IgxButtonDirective) private templateButtons: QueryList<IgxButtonDirective>;
302

303
    /**
304
     * Returns true if the `igx-buttongroup` alignment is vertical.
305
     * Note that in order for the accessor to work correctly the property should be set explicitly.
306
     * ```html
307
     * <igx-buttongroup #MyChild [alignment]="alignment" [values]="alignOptions">
308
     * ```
309
     * ```typescript
310
     * //...
311
     * @ViewChild("MyChild")
312
     * private buttonG: IgxButtonGroupComponent;
313
     * ngAfterViewInit(){
314
     *    let orientation = this.buttonG.isVertical;
315
     * }
316
     * ```
317
     */
318
    @HostBinding('class.igx-button-group--vertical')
319
    public get isVertical(): boolean {
320
        return this._isVertical;
506✔
321
    }
322

323
    /**
324
     * @hidden
325
     */
326
    public selectedIndexes: number[] = [];
97✔
327

328
    protected buttonClickNotifier$ = new Subject<void>();
97✔
329
    protected queryListNotifier$ = new Subject<void>();
97✔
330

331
    private _isVertical: boolean;
332
    private _itemContentCssClass: string;
333
    private _disabled = false;
97✔
334
    private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
97✔
335

336
    private mutationObserver: MutationObserver;
337
    private observerConfig: MutationObserverInit = {
97✔
338
      attributeFilter: ["data-selected"],
339
      childList: true,
340
      subtree: true,
341
    };
342

343
    /**
344
     * Gets the selected button/buttons.
345
     * ```typescript
346
     * @ViewChild("MyChild")
347
     * private buttonG: IgxButtonGroupComponent;
348
     * ngAfterViewInit(){
349
     *    let selectedButton = this.buttonG.selectedButtons;
350
     * }
351
     * ```
352
     */
353
    public get selectedButtons(): IgxButtonDirective[] {
354
        return this.buttons.filter((_, i) => this.selectedIndexes.indexOf(i) !== -1);
124✔
355
    }
356

357
    /**
358
     * Selects a button by its index.
359
     * ```typescript
360
     * @ViewChild("MyChild")
361
     * private buttonG: IgxButtonGroupComponent;
362
     * ngAfterViewInit(){
363
     *    this.buttonG.selectButton(2);
364
     *    this.cdr.detectChanges();
365
     * }
366
     * ```
367
     *
368
     * @memberOf {@link IgxButtonGroupComponent}
369
     */
370
    public selectButton(index: number) {
371
        if (index >= this.buttons.length || index < 0) {
61✔
372
            return;
2✔
373
        }
374

375
        const button = this.buttons[index];
59✔
376
        button.select();
59✔
377
    }
378

379
    /**
380
     * @hidden
381
     * @internal
382
     */
383
    public updateSelected(index: number) {
384
        const button = this.buttons[index];
106✔
385

386
        if (this.selectedIndexes.indexOf(index) === -1) {
106✔
387
            this.selectedIndexes.push(index);
95✔
388
        }
389

390
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
106✔
391
        this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
106✔
392

393
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
106✔
394
        if (indexInViewButtons !== -1) {
106✔
395
            this.values[indexInViewButtons].selected = true;
17✔
396
        }
397

398
        // deselect other buttons if selectionMode is not multi
399
        if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
106✔
400
            this.buttons.forEach((_, i) => {
14✔
401
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
41✔
402
                    this.deselectButton(i);
14✔
403
                    this.updateDeselected(i);
14✔
404
                }
405
            });
406
        }
407

408
    }
409

410
    public updateDeselected(index: number) {
411
        const button = this.buttons[index];
35✔
412
        if (this.selectedIndexes.indexOf(index) !== -1) {
35✔
413
            this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
31✔
414
        }
415

416
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
35✔
417
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
35✔
418

419
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
35✔
420
        if (indexInViewButtons !== -1) {
35✔
421
            this.values[indexInViewButtons].selected = false;
10✔
422
        }
423
    }
424

425
    /**
426
     * Deselects a button by its index.
427
     * ```typescript
428
     * @ViewChild("MyChild")
429
     * private buttonG: IgxButtonGroupComponent;
430
     * ngAfterViewInit(){
431
     *    this.buttonG.deselectButton(2);
432
     *    this.cdr.detectChanges();
433
     * }
434
     * ```
435
     *
436
     * @memberOf {@link IgxButtonGroupComponent}
437
     */
438
    public deselectButton(index: number) {
439
        if (index >= this.buttons.length || index < 0) {
125✔
440
            return;
2✔
441
        }
442

443
        const button = this.buttons[index];
123✔
444
        button.deselect();
123✔
445
    }
446

447
    /**
448
     * @hidden
449
     */
450
    public ngAfterViewInit() {
451
        const initButtons = () => {
99✔
452
            // Cancel any existing buttonClick subscriptions
453
            this.buttonClickNotifier$.next();
99✔
454

455
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
99✔
456

457
            // initial configuration
458
            this.buttons.forEach((button, index) => {
99✔
459
                const buttonElement = button.nativeElement;
222✔
460
                this._renderer.addClass(buttonElement, 'igx-button-group__item');
222✔
461

462
                if (this.disabled) {
222!
463
                    button.disabled = true;
×
464
                }
465

466
                if (button.selected) {
222✔
467
                    this.updateSelected(index);
59✔
468
                }
469

470
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
222✔
471
            });
472
        };
473

474
        this.mutationObserver = this.setMutationsObserver();
99✔
475

476
        this.viewButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
99✔
477
            this.mutationObserver.disconnect();
×
478
            initButtons();
×
479
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
480
        });
481
        this.templateButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
99✔
482
            this.mutationObserver.disconnect();
×
483
            initButtons();
×
484
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
485
        });
486

487
        initButtons();
99✔
488
        this._cdr.detectChanges();
99✔
489
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
99✔
490
    }
491

492
    /**
493
     * @hidden
494
     */
495
    public ngOnDestroy() {
496
        this.buttonClickNotifier$.next();
97✔
497
        this.buttonClickNotifier$.complete();
97✔
498

499
        this.queryListNotifier$.next();
97✔
500
        this.queryListNotifier$.complete();
97✔
501

502
        this.mutationObserver?.disconnect();
97✔
503
    }
504

505
    /**
506
     * @hidden
507
     */
508
    public _clickHandler(index: number) {
509
        const button = this.buttons[index];
37✔
510
        const args: IButtonGroupEventArgs = { owner: this, button, index };
37✔
511

512
        if (this.selectionMode !== 'multi') {
37✔
513
            this.buttons.forEach((b, i) => {
32✔
514
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
70✔
515
                    this.deselected.emit({ owner: this, button: b, index: i });
6✔
516
                }
517
            });
518
        }
519

520
        if (this.selectedIndexes.indexOf(index) === -1) {
37✔
521
            this.selectButton(index);
30✔
522
            this.selected.emit(args);
30✔
523
        } else {
524
            if (this.selectionMode !== 'singleRequired') {
7✔
525
                this.deselectButton(index);
6✔
526
                this.deselected.emit(args);
6✔
527
            }
528
        }
529
    }
530

531
    private setMutationsObserver() {
532
        if (typeof MutationObserver !== 'undefined') {
99✔
533
            return new MutationObserver((records, observer) => {
99✔
534
                // Stop observing while handling changes
535
                observer.disconnect();
40✔
536

537
                const updatedButtons = this.getUpdatedButtons(records);
40✔
538

539
                if (updatedButtons.length > 0) {
40✔
540
                    updatedButtons.forEach((button) => {
40✔
541
                        const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
204✔
542

543
                        this.updateButtonSelectionState(index);
68✔
544
                    });
545
                }
546

547
                // Watch for changes again
548
                observer.observe(this._el.nativeElement, this.observerConfig);
40✔
549
            });
550
        }
551
    }
552

553
    private getUpdatedButtons(records: MutationRecord[]) {
554
        const updated: HTMLButtonElement[] = [];
40✔
555

556
        records
40✔
557
          .filter((x) => x.type === 'attributes')
68✔
558
          .reduce((prev, curr) => {
559
            prev.push(
68✔
560
              curr.target as HTMLButtonElement
561
            );
562
            return prev;
68✔
563
          }, updated);
564

565
        return updated;
40✔
566
    }
567

568
    private updateButtonSelectionState(index: number) {
569
        if (this.buttons[index].selected) {
68✔
570
            this.updateSelected(index);
47✔
571
        } else {
572
            this.updateDeselected(index);
21✔
573
        }
574
    }
575
}
576

577
export interface IButtonGroupEventArgs extends IBaseEventArgs {
578
    owner: IgxButtonGroupComponent;
579
    button: IgxButtonDirective;
580
    index: number;
581
}
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