Skip to content

Commit 9831fcc

Browse files
authored
fix(buttonGroup): Resolve the issue with the emission of buttonSelected on init (#13861)
1 parent 0324a6b commit 9831fcc

File tree

5 files changed

+122
-35
lines changed

5 files changed

+122
-35
lines changed

projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,20 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
301301
public selectedIndexes: number[] = [];
302302

303303
protected buttonClickNotifier$ = new Subject<boolean>();
304-
protected buttonSelectedNotifier$ = new Subject<boolean>();
305304
protected queryListNotifier$ = new Subject<boolean>();
306305

307306
private _isVertical: boolean;
308307
private _itemContentCssClass: string;
309308
private _disabled = false;
310309
private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
311310

311+
private mutationObserver: MutationObserver;
312+
private observerConfig: MutationObserverInit = {
313+
attributeFilter: ["data-selected"],
314+
childList: true,
315+
subtree: true,
316+
};
317+
312318
constructor(
313319
private _cdr: ChangeDetectorRef,
314320
private _renderer: Renderer2,
@@ -350,6 +356,8 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
350356
return;
351357
}
352358

359+
this.updateSelected(index);
360+
353361
const button = this.buttons[index];
354362
button.select();
355363
}
@@ -365,25 +373,21 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
365373
this.selectedIndexes.push(index);
366374
}
367375

368-
if (button.selected) {
369-
this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
370-
this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
376+
this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
377+
this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
371378

372-
const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
373-
if (indexInViewButtons !== -1) {
379+
const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
380+
if (indexInViewButtons !== -1) {
374381
this.values[indexInViewButtons].selected = true;
375-
}
382+
}
376383

377-
// deselect other buttons if selectionMode is not multi
378-
if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
379-
this.buttons.forEach((_, i) => {
380-
if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
381-
this.deselectButton(i);
382-
}
383-
});
384-
}
385-
} else {
386-
this.deselectButton(index);
384+
// deselect other buttons if selectionMode is not multi
385+
if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
386+
this.buttons.forEach((_, i) => {
387+
if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
388+
this.deselectButton(i);
389+
}
390+
});
387391
}
388392
}
389393

@@ -453,9 +457,6 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
453457
}
454458

455459
button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
456-
button.buttonSelected
457-
.pipe(takeUntil(this.buttonSelectedNotifier$))
458-
.subscribe((_) => this.updateSelected(index));
459460
});
460461
};
461462

@@ -464,6 +465,10 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
464465
initButtons();
465466

466467
this._cdr.detectChanges();
468+
469+
this.mutationObserver = this.setMutationsObserver();
470+
471+
this.mutationObserver.observe(this._el.nativeElement, this.observerConfig);
467472
}
468473

469474
/**
@@ -473,17 +478,18 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
473478
this.buttonClickNotifier$.next();
474479
this.buttonClickNotifier$.complete();
475480

476-
this.buttonSelectedNotifier$.next();
477-
this.buttonSelectedNotifier$.complete();
478-
479481
this.queryListNotifier$.next();
480482
this.queryListNotifier$.complete();
483+
484+
this.mutationObserver.disconnect();
481485
}
482486

483487
/**
484488
* @hidden
485489
*/
486490
public _clickHandler(index: number) {
491+
this.mutationObserver.disconnect();
492+
487493
const button = this.buttons[index];
488494
const args: IButtonGroupEventArgs = { owner: this, button, index };
489495

@@ -504,6 +510,54 @@ export class IgxButtonGroupComponent extends DisplayDensityBase implements After
504510
this.deselected.emit(args);
505511
}
506512
}
513+
514+
this.mutationObserver.observe(this._el.nativeElement, this.observerConfig);
515+
}
516+
517+
private setMutationsObserver() {
518+
return new MutationObserver((records, observer) => {
519+
// Stop observing while handling changes
520+
observer.disconnect();
521+
522+
const updatedButtons = this.getUpdatedButtons(records);
523+
524+
if (updatedButtons.length > 0) {
525+
updatedButtons.forEach((button) => {
526+
const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
527+
const args: IButtonGroupEventArgs = { owner: this, button: this.buttons[index], index };
528+
529+
this.updateButtonSelectionState(index, args);
530+
});
531+
}
532+
533+
// Watch for changes again
534+
observer.observe(this._el.nativeElement, this.observerConfig);
535+
});
536+
}
537+
538+
private getUpdatedButtons(records: MutationRecord[]) {
539+
const updated: HTMLButtonElement[] = [];
540+
541+
records
542+
.filter((x) => x.type === 'attributes')
543+
.reduce((prev, curr) => {
544+
prev.push(
545+
curr.target as HTMLButtonElement
546+
);
547+
return prev;
548+
}, updated);
549+
550+
return updated;
551+
}
552+
553+
private updateButtonSelectionState(index: number, args: IButtonGroupEventArgs) {
554+
if (this.selectedIndexes.indexOf(index) === -1) {
555+
this.selectButton(index);
556+
this.selected.emit(args);
557+
} else {
558+
this.deselectButton(index);
559+
this.deselected.emit(args);
560+
}
507561
}
508562
}
509563

projects/igniteui-angular/src/lib/buttonGroup/buttongroup.component.spec.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, OnInit, ViewChild } from '@angular/core';
2-
import { TestBed, waitForAsync } from '@angular/core/testing';
2+
import { TestBed, fakeAsync, flushMicrotasks, waitForAsync } from '@angular/core/testing';
33
import { ButtonGroupAlignment, IgxButtonGroupComponent } from './buttonGroup.component';
44
import { configureTestSuite } from '../test-utils/configure-suite';
55
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -115,7 +115,11 @@ describe('IgxButtonGroup', () => {
115115

116116
const button = fixture.debugElement.nativeElement.querySelector('button');
117117
button.click();
118+
// The first button is already selected, so it should not fire the selected event, but the deselected one.
119+
expect(btnGroupInstance.selected.emit).not.toHaveBeenCalled();
118120

121+
const unselectedButton = fixture.debugElement.nativeElement.querySelector('#unselected');
122+
unselectedButton.click();
119123
expect(btnGroupInstance.selected.emit).toHaveBeenCalled();
120124
});
121125

@@ -360,7 +364,7 @@ describe('IgxButtonGroup', () => {
360364
}
361365
});
362366

363-
it('should style the corresponding button as deselected when the value bound to the selected input changes', () => {
367+
it('should style the corresponding button as deselected when the value bound to the selected input changes', fakeAsync(() => {
364368
const fixture = TestBed.createComponent(ButtonGroupButtonWithBoundSelectedOutputComponent);
365369
fixture.detectChanges();
366370

@@ -370,11 +374,13 @@ describe('IgxButtonGroup', () => {
370374
expect(btnGroupInstance.buttons[1].selected).toBe(true);
371375

372376
fixture.componentInstance.selectedValue = 100;
377+
flushMicrotasks();
373378
fixture.detectChanges();
374379

375-
expect(btnGroupInstance.selectedButtons.length).toBe(0);
376-
expect(btnGroupInstance.buttons[1].selected).toBe(false);
377-
});
380+
btnGroupInstance.buttons.forEach((button) => {
381+
expect(button.selected).toBe(false);
382+
});
383+
}));
378384

379385
});
380386

@@ -492,7 +498,7 @@ class TemplatedButtonGroupDesplayDensityComponent {
492498
template: `
493499
<igx-buttongroup>
494500
<button igxButton [selected]="true">Button 0</button>
495-
<button igxButton>Button 1</button>
501+
<button igxButton id="unselected">Button 1</button>
496502
<button igxButton>Button 2</button>
497503
</igx-buttongroup>
498504
`,

projects/igniteui-angular/src/lib/directives/button/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ this.button.displayDensity = "compact";
3131
| `igxButtonColor` | string | Set the button text color. You can pass any CSS valid color value. |
3232
| `igxButtonBackground` | string | Set the button background color. You can pass any CSS valid color value. |
3333
| `displayDensity` | DisplayDensity | Determines the display density of the button. |
34+
| `buttonSelected` | EventEmitter<IButtonEventArgs> | Emitted only when a button gets selected, or deselected, and not on initialization. |
35+
| `selected` | boolean | Gets or sets whether the button is selected. Mainly used in the IgxButtonGroup component and it will have no effect if set separately. |
3436

3537
# Button types
3638
| Name | Description |

projects/igniteui-angular/src/lib/directives/button/button.directive.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,24 @@ describe('IgxButton', () => {
146146
expect(theButtonNativeEl.classList.length).toEqual(2);
147147
expect(theButtonNativeEl.classList).toContain(classes.flat);
148148
});
149+
150+
it('Should emit the buttonSelected event only on user interaction, not on initialization', () => {
151+
const fixture = TestBed.createComponent(InitButtonComponent);
152+
fixture.detectChanges();
153+
const button = fixture.componentInstance.button;
154+
spyOn(button.buttonSelected, 'emit');
155+
156+
button.ngOnInit();
157+
expect(button.buttonSelected.emit).not.toHaveBeenCalled();
158+
159+
button.nativeElement.click();
160+
fixture.detectChanges();
161+
expect(button.buttonSelected.emit).toHaveBeenCalledTimes(1);
162+
163+
button.nativeElement.click();
164+
fixture.detectChanges();
165+
expect(button.buttonSelected.emit).toHaveBeenCalledTimes(2);
166+
});
149167
});
150168

151169
@Component({

projects/igniteui-angular/src/lib/directives/button/button.directive.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
Renderer2,
99
HostListener,
1010
Optional,
11-
Inject
11+
Inject,
12+
AfterContentInit
1213
} from '@angular/core';
1314
import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../../core/density';
1415
import { mkenum } from '../../core/utils';
@@ -50,7 +51,7 @@ export type IgxButtonType = typeof IgxButtonType[keyof typeof IgxButtonType];
5051
selector: '[igxButton]',
5152
standalone: true
5253
})
53-
export class IgxButtonDirective extends DisplayDensityBase {
54+
export class IgxButtonDirective extends DisplayDensityBase implements AfterContentInit {
5455
private static ngAcceptInputType_type: IgxButtonType | '';
5556
private static ngAcceptInputType_disabled: boolean | '';
5657

@@ -135,9 +136,7 @@ export class IgxButtonDirective extends DisplayDensityBase {
135136
if(this._selected !== value) {
136137
this._selected = value;
137138

138-
this.buttonSelected.emit({
139-
button: this
140-
});
139+
this._renderer.setAttribute(this.nativeElement, 'data-selected', value.toString());
141140
}
142141
}
143142

@@ -153,6 +152,14 @@ export class IgxButtonDirective extends DisplayDensityBase {
153152
super(_displayDensityOptions, element);
154153
}
155154

155+
public ngAfterContentInit() {
156+
this.nativeElement.addEventListener('click', () => {
157+
this.buttonSelected.emit({
158+
button: this
159+
});
160+
});
161+
}
162+
156163
/**
157164
* @hidden
158165
* @internal
@@ -329,7 +336,7 @@ export class IgxButtonDirective extends DisplayDensityBase {
329336
* @internal
330337
*/
331338
public deselect() {
332-
this._selected = false;
339+
this.selected = false;
333340
}
334341
}
335342

0 commit comments

Comments
 (0)