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

IgniteUI / igniteui-angular / 13561607909

27 Feb 2025 08:03AM UTC coverage: 91.644% (+0.003%) from 91.641%
13561607909

push

github

web-flow
fix(grid): Update grid cell active state selector specificity (#15402)

13328 of 15596 branches covered (85.46%)

26882 of 29333 relevant lines covered (91.64%)

33708.8 hits per line

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

86.61
/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.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
} from '@angular/core';
17
import { Subject } from 'rxjs';
18
import { IgxButtonDirective } from '../directives/button/button.directive';
19
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
20

21
import { takeUntil } from 'rxjs/operators';
22
import { IBaseEventArgs } from '../core/utils';
23
import { mkenum } from '../core/utils';
24
import { IgxIconComponent } from '../icon/icon.component';
25

26
/**
27
 * Determines the Button Group alignment
28
 */
29
export const ButtonGroupAlignment = mkenum({
2✔
30
    horizontal: 'horizontal',
31
    vertical: 'vertical'
32
});
33
export type ButtonGroupAlignment = typeof ButtonGroupAlignment[keyof typeof ButtonGroupAlignment];
34

35
let NEXT_ID = 0;
2✔
36

37
/**
38
 * **Ignite UI for Angular Button Group** -
39
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html)
40
 *
41
 * The Ignite UI Button Group displays a group of buttons either vertically or horizontally. The group supports
42
 * single, multi and singleRequired selection.
43
 *
44
 * Example:
45
 * ```html
46
 * <igx-buttongroup selectionMode="multi" [values]="fontOptions">
47
 * </igx-buttongroup>
48
 * ```
49
 * The `fontOptions` value shown above is defined as:
50
 * ```typescript
51
 * this.fontOptions = [
52
 *   { icon: 'format_bold', selected: false },
53
 *   { icon: 'format_italic', selected: false },
54
 *   { icon: 'format_underlined', selected: false }];
55
 * ```
56
 */
57
@Component({
58
    selector: 'igx-buttongroup',
59
    templateUrl: 'buttongroup-content.component.html',
60
    imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent]
61
})
62
export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
2✔
63
    /**
64
     * A collection containing all buttons inside the button group.
65
     */
66
    public get buttons(): IgxButtonDirective[] {
67
        return [...this.viewButtons.toArray(), ...this.templateButtons.toArray()];
969✔
68
    }
69

70
    /**
71
     * Gets/Sets the value of the `id` attribute. If not set it will be automatically generated.
72
     * ```html
73
     *  <igx-buttongroup [id]="'igx-dialog-56'" [selectionMode]="'multi'" [values]="alignOptions">
74
     * ```
75
     */
76
    @HostBinding('attr.id')
77
    @Input()
78
    public id = `igx-buttongroup-${NEXT_ID++}`;
92✔
79

80
    /**
81
     * @hidden
82
     */
83
    @HostBinding('style.zIndex')
84
    public zIndex = 0;
92✔
85

86
    /**
87
     * Allows you to set a style using the `itemContentCssClass` input.
88
     * The value should be the CSS class name that will be applied to the button group.
89
     * ```typescript
90
     * public style1 = "styleClass";
91
     *  //..
92
     * ```
93
     *  ```html
94
     * <igx-buttongroup [itemContentCssClass]="style1" [selectionMode]="'multi'" [values]="alignOptions">
95
     * ```
96
     */
97
    @Input()
98
    public set itemContentCssClass(value: string) {
99
        this._itemContentCssClass = value || this._itemContentCssClass;
4!
100
    }
101

102
    /**
103
     * Returns the CSS class of the item content of the `IgxButtonGroup`.
104
     * ```typescript
105
     *  @ViewChild("MyChild")
106
     * public buttonG: IgxButtonGroupComponent;
107
     * ngAfterViewInit(){
108
     *    let buttonSelect = this.buttonG.itemContentCssClass;
109
     * }
110
     * ```
111
     */
112
    public get itemContentCssClass(): string {
113
        return this._itemContentCssClass;
104✔
114
    }
115

116
    /**
117
     * Enables selecting multiple buttons. By default, multi-selection is false.
118
     *
119
     * @deprecated in version 16.1.0. Use the `selectionMode` property instead.
120
     */
121
    @Input()
122
    public get multiSelection() {
123
        if (this.selectionMode === 'multi') {
×
124
            return true;
×
125
        } else {
126
            return false;
×
127
        }
128
    }
129
    public set multiSelection(selectionMode: boolean) {
130
        if (selectionMode) {
×
131
            this.selectionMode = 'multi';
×
132
        } else {
133
            this.selectionMode = 'single';
×
134
        }
135
    }
136

137
    /**
138
     * Gets/Sets the selection mode to 'single', 'singleRequired' or 'multi' of the buttons. By default, the selection mode is 'single'.
139
     * ```html
140
     * <igx-buttongroup [selectionMode]="'multi'" [alignment]="alignment"></igx-buttongroup>
141
     * ```
142
     */
143
    @Input()
144
    public get selectionMode() {
145
        return this._selectionMode;
151✔
146
    }
147
    public set selectionMode(selectionMode: 'single' | 'singleRequired' | 'multi') {
148
        if (this.viewButtons && selectionMode !== this._selectionMode) {
12✔
149
            this.buttons.forEach((b,i) => {
4✔
150
                this.deselectButton(i);
13✔
151
            });
152
            this._selectionMode = selectionMode;
4✔
153
        } else {
154
            this._selectionMode = selectionMode;
8✔
155
        }
156
    }
157

158
    /**
159
     * Property that configures the buttons in the button group using a collection of `Button` objects.
160
     * ```typescript
161
     *  public ngOnInit() {
162
     *      this.cities = [
163
     *        new Button({
164
     *          label: "Sofia"
165
     *      }),
166
     *        new Button({
167
     *          label: "London"
168
     *      }),
169
     *        new Button({
170
     *          label: "New York",
171
     *          selected: true
172
     *      }),
173
     *        new Button({
174
     *          label: "Tokyo"
175
     *      })
176
     *  ];
177
     *  }
178
     *  //..
179
     * ```
180
     * ```html
181
     *  <igx-buttongroup [selectionMode]="'single'" [values]="cities"></igx-buttongroup>
182
     * ```
183
     */
184
    @Input() public values: any;
185

186
    /**
187
     * Disables the `igx-buttongroup` component. By default it's false.
188
     * ```html
189
     * <igx-buttongroup [disabled]="true" [selectionMode]="'multi'" [values]="fontOptions"></igx-buttongroup>
190
     * ```
191
     */
192
    @Input({ transform: booleanAttribute })
193
    public get disabled(): boolean {
194
        return this._disabled;
316✔
195
    }
196
    public set disabled(value: boolean) {
197
        if (this._disabled !== value) {
×
198
            this._disabled = value;
×
199

200
            if (this.viewButtons && this.templateButtons) {
×
201
                this.buttons.forEach((b) => (b.disabled = this._disabled));
×
202
            }
203
        }
204
    }
205

206
    /**
207
     * Allows you to set the button group alignment.
208
     * Available options are `ButtonGroupAlignment.horizontal` (default) and `ButtonGroupAlignment.vertical`.
209
     * ```typescript
210
     * public alignment = ButtonGroupAlignment.vertical;
211
     * //..
212
     * ```
213
     * ```html
214
     * <igx-buttongroup [selectionMode]="'single'" [values]="cities" [alignment]="alignment"></igx-buttongroup>
215
     * ```
216
     */
217
    @Input()
218
    public set alignment(value: ButtonGroupAlignment) {
219
        this._isVertical = value === ButtonGroupAlignment.vertical;
7✔
220
    }
221
    /**
222
     * Returns the alignment of the `igx-buttongroup`.
223
     * ```typescript
224
     * @ViewChild("MyChild")
225
     * public buttonG: IgxButtonGroupComponent;
226
     * ngAfterViewInit(){
227
     *    let buttonAlignment = this.buttonG.alignment;
228
     * }
229
     * ```
230
     */
231
    public get alignment(): ButtonGroupAlignment {
232
        return this._isVertical ? ButtonGroupAlignment.vertical : ButtonGroupAlignment.horizontal;
2✔
233
    }
234

235
    /**
236
     * An @Ouput property that emits an event when a button is selected.
237
     * ```typescript
238
     * @ViewChild("toast")
239
     * private toast: IgxToastComponent;
240
     * public selectedHandler(buttongroup) {
241
     *     this.toast.open()
242
     * }
243
     *  //...
244
     * ```
245
     * ```html
246
     * <igx-buttongroup #MyChild [selectionMode]="'multi'" (selected)="selectedHandler($event)"></igx-buttongroup>
247
     * <igx-toast #toast>You have made a selection!</igx-toast>
248
     * ```
249
     */
250
    @Output()
251
    public selected = new EventEmitter<IButtonGroupEventArgs>();
92✔
252

253
    /**
254
     * An @Ouput property that emits an event when a button is deselected.
255
     * ```typescript
256
     *  @ViewChild("toast")
257
     *  private toast: IgxToastComponent;
258
     *  public deselectedHandler(buttongroup){
259
     *     this.toast.open()
260
     * }
261
     *  //...
262
     * ```
263
     * ```html
264
     * <igx-buttongroup> #MyChild [selectionMode]="'multi'" (deselected)="deselectedHandler($event)"></igx-buttongroup>
265
     * <igx-toast #toast>You have deselected a button!</igx-toast>
266
     * ```
267
     */
268
    @Output()
269
    public deselected = new EventEmitter<IButtonGroupEventArgs>();
92✔
270

271
    @ViewChildren(IgxButtonDirective) private viewButtons: QueryList<IgxButtonDirective>;
272
    @ContentChildren(IgxButtonDirective) private templateButtons: QueryList<IgxButtonDirective>;
273

274
    /**
275
     * Returns true if the `igx-buttongroup` alignment is vertical.
276
     * Note that in order for the accessor to work correctly the property should be set explicitly.
277
     * ```html
278
     * <igx-buttongroup #MyChild [alignment]="alignment" [values]="alignOptions">
279
     * ```
280
     * ```typescript
281
     * //...
282
     * @ViewChild("MyChild")
283
     * private buttonG: IgxButtonGroupComponent;
284
     * ngAfterViewInit(){
285
     *    let orientation = this.buttonG.isVertical;
286
     * }
287
     * ```
288
     */
289
    public get isVertical(): boolean {
290
        return this._isVertical;
566✔
291
    }
292

293
    /**
294
     * @hidden
295
     */
296
    public selectedIndexes: number[] = [];
92✔
297

298
    protected buttonClickNotifier$ = new Subject<void>();
92✔
299
    protected queryListNotifier$ = new Subject<void>();
92✔
300

301
    private _isVertical: boolean;
302
    private _itemContentCssClass: string;
303
    private _disabled = false;
92✔
304
    private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
92✔
305

306
    private mutationObserver: MutationObserver;
307
    private observerConfig: MutationObserverInit = {
92✔
308
      attributeFilter: ["data-selected"],
309
      childList: true,
310
      subtree: true,
311
    };
312

313
    constructor(
314
        private _cdr: ChangeDetectorRef,
92✔
315
        private _renderer: Renderer2,
92✔
316
        private _el: ElementRef
92✔
317
    ) {}
318

319
    /**
320
     * Gets the selected button/buttons.
321
     * ```typescript
322
     * @ViewChild("MyChild")
323
     * private buttonG: IgxButtonGroupComponent;
324
     * ngAfterViewInit(){
325
     *    let selectedButton = this.buttonG.selectedButtons;
326
     * }
327
     * ```
328
     */
329
    public get selectedButtons(): IgxButtonDirective[] {
330
        return this.buttons.filter((_, i) => this.selectedIndexes.indexOf(i) !== -1);
124✔
331
    }
332

333
    /**
334
     * Selects a button by its index.
335
     * ```typescript
336
     * @ViewChild("MyChild")
337
     * private buttonG: IgxButtonGroupComponent;
338
     * ngAfterViewInit(){
339
     *    this.buttonG.selectButton(2);
340
     *    this.cdr.detectChanges();
341
     * }
342
     * ```
343
     *
344
     * @memberOf {@link IgxButtonGroupComponent}
345
     */
346
    public selectButton(index: number) {
347
        if (index >= this.buttons.length || index < 0) {
62✔
348
            return;
2✔
349
        }
350

351
        const button = this.buttons[index];
60✔
352
        button.select();
60✔
353
    }
354

355
    /**
356
     * @hidden
357
     * @internal
358
     */
359
    public updateSelected(index: number) {
360
        const button = this.buttons[index];
101✔
361

362
        if (this.selectedIndexes.indexOf(index) === -1) {
101✔
363
            this.selectedIndexes.push(index);
90✔
364
        }
365

366
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
101✔
367
        this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
101✔
368

369
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
101✔
370
        if (indexInViewButtons !== -1) {
101✔
371
            this.values[indexInViewButtons].selected = true;
17✔
372
        }
373

374
        // deselect other buttons if selectionMode is not multi
375
        if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
101✔
376
            this.buttons.forEach((_, i) => {
14✔
377
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
41✔
378
                    this.deselectButton(i);
14✔
379
                    this.updateDeselected(i);
14✔
380
                }
381
            });
382
        }
383

384
    }
385

386
    public updateDeselected(index: number) {
387
        const button = this.buttons[index];
35✔
388
        if (this.selectedIndexes.indexOf(index) !== -1) {
35✔
389
            this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
31✔
390
        }
391

392
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
35✔
393
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
35✔
394

395
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
35✔
396
        if (indexInViewButtons !== -1) {
35✔
397
            this.values[indexInViewButtons].selected = false;
10✔
398
        }
399
    }
400

401
    /**
402
     * Deselects a button by its index.
403
     * ```typescript
404
     * @ViewChild("MyChild")
405
     * private buttonG: IgxButtonGroupComponent;
406
     * ngAfterViewInit(){
407
     *    this.buttonG.deselectButton(2);
408
     *    this.cdr.detectChanges();
409
     * }
410
     * ```
411
     *
412
     * @memberOf {@link IgxButtonGroupComponent}
413
     */
414
    public deselectButton(index: number) {
415
        if (index >= this.buttons.length || index < 0) {
137✔
416
            return;
2✔
417
        }
418

419
        const button = this.buttons[index];
135✔
420
        button.deselect();
135✔
421
    }
422

423
    /**
424
     * @hidden
425
     */
426
    public ngAfterViewInit() {
427
        const initButtons = () => {
94✔
428
            // Cancel any existing buttonClick subscriptions
429
            this.buttonClickNotifier$.next();
94✔
430

431
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
94✔
432

433
            // initial configuration
434
            this.buttons.forEach((button, index) => {
94✔
435
                const buttonElement = button.nativeElement;
212✔
436
                this._renderer.addClass(buttonElement, 'igx-button-group__item');
212✔
437

438
                if (this.disabled) {
212!
439
                    button.disabled = true;
×
440
                }
441

442
                if (button.selected) {
212✔
443
                    this.updateSelected(index);
54✔
444
                }
445

446
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
212✔
447
            });
448
        };
449

450
        this.mutationObserver = this.setMutationsObserver();
94✔
451

452
        this.viewButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
94✔
453
            this.mutationObserver.disconnect();
×
454
            initButtons();
×
455
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
456
        });
457
        this.templateButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
94✔
458
            this.mutationObserver.disconnect();
×
459
            initButtons();
×
460
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
461
        });
462

463
        initButtons();
94✔
464
        this._cdr.detectChanges();
94✔
465
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
94✔
466
    }
467

468
    /**
469
     * @hidden
470
     */
471
    public ngOnDestroy() {
472
        this.buttonClickNotifier$.next();
92✔
473
        this.buttonClickNotifier$.complete();
92✔
474

475
        this.queryListNotifier$.next();
92✔
476
        this.queryListNotifier$.complete();
92✔
477

478
        this.mutationObserver?.disconnect();
92✔
479
    }
480

481
    /**
482
     * @hidden
483
     */
484
    public _clickHandler(index: number) {
485
        const button = this.buttons[index];
37✔
486
        const args: IButtonGroupEventArgs = { owner: this, button, index };
37✔
487

488
        if (this.selectionMode !== 'multi') {
37✔
489
            this.buttons.forEach((b, i) => {
32✔
490
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
70✔
491
                    this.deselected.emit({ owner: this, button: b, index: i });
6✔
492
                }
493
            });
494
        }
495

496
        if (this.selectedIndexes.indexOf(index) === -1) {
37✔
497
            this.selectButton(index);
30✔
498
            this.selected.emit(args);
30✔
499
        } else {
500
            if (this.selectionMode !== 'singleRequired') {
7✔
501
                this.deselectButton(index);
6✔
502
                this.deselected.emit(args);
6✔
503
            }
504
        }
505
    }
506

507
    private setMutationsObserver() {
508
        if (typeof MutationObserver !== 'undefined') {
94✔
509
            return new MutationObserver((records, observer) => {
94✔
510
                // Stop observing while handling changes
511
                observer.disconnect();
40✔
512

513
                const updatedButtons = this.getUpdatedButtons(records);
40✔
514

515
                if (updatedButtons.length > 0) {
40✔
516
                    updatedButtons.forEach((button) => {
40✔
517
                        const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
204✔
518

519
                        this.updateButtonSelectionState(index);
68✔
520
                    });
521
                }
522

523
                // Watch for changes again
524
                observer.observe(this._el.nativeElement, this.observerConfig);
40✔
525
            });
526
        }
527
    }
528

529
    private getUpdatedButtons(records: MutationRecord[]) {
530
        const updated: HTMLButtonElement[] = [];
40✔
531

532
        records
40✔
533
          .filter((x) => x.type === 'attributes')
68✔
534
          .reduce((prev, curr) => {
535
            prev.push(
68✔
536
              curr.target as HTMLButtonElement
537
            );
538
            return prev;
68✔
539
          }, updated);
540

541
        return updated;
40✔
542
    }
543

544
    private updateButtonSelectionState(index: number) {
545
        if (this.buttons[index].selected) {
68✔
546
            this.updateSelected(index);
47✔
547
        } else {
548
            this.updateDeselected(index);
21✔
549
        }
550
    }
551
}
552

553
export interface IButtonGroupEventArgs extends IBaseEventArgs {
554
    owner: IgxButtonGroupComponent;
555
    button: IgxButtonDirective;
556
    index: number;
557
}
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

© 2025 Coveralls, Inc