Skip to content

Commit afed818

Browse files
devversionjelbourn
authored andcommitted
fix(checkbox): prevent double click events (#672)
* fix(slide-toggle and checkbox): visual hidden input should not bubble click * Currently the (click) event gets called twice. This is caused by the bubbling of the (click) event on the input. Fixes #671. * update(): move input click into component source * Moved input click event into component source file. * Added comment for the issue cause. * fix(checkbox): do not trigger the click event twice
1 parent 5c4dc04 commit afed818

File tree

6 files changed

+78
-5
lines changed

6 files changed

+78
-5
lines changed

src/components/checkbox/checkbox.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
[attr.aria-labelledby]="ariaLabelledby"
1212
(focus)="onInputFocus()"
1313
(blur)="onInputBlur()"
14-
(change)="onInteractionEvent($event)">
14+
(change)="onInteractionEvent($event)"
15+
(click)="onInputClick($event)">
1516
<div class="md-ink-ripple"></div>
1617
<div class="md-checkbox-frame"></div>
1718
<div class="md-checkbox-background">

src/components/checkbox/checkbox.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,26 @@ describe('MdCheckbox', () => {
183183
expect(checkboxNativeElement.classList).toContain('md-checkbox-align-end');
184184
});
185185

186+
it('should not trigger the click event multiple times', () => {
187+
// By default, when clicking on a label element, a generated click will be dispatched
188+
// on the associated input element.
189+
// Since we're using a label element and a visual hidden input, this behavior can led
190+
// to an issue, where the click events on the checkbox are getting executed twice.
191+
192+
spyOn(testComponent, 'onCheckboxClick');
193+
194+
expect(inputElement.checked).toBe(false);
195+
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
196+
197+
labelElement.click();
198+
fixture.detectChanges();
199+
200+
expect(checkboxNativeElement.classList).toContain('md-checkbox-checked');
201+
expect(inputElement.checked).toBe(true);
202+
203+
expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1);
204+
});
205+
186206
it('should emit a change event when the `checked` value changes', () => {
187207
// TODO(jelbourn): this *should* work with async(), but fixture.whenStable currently doesn't
188208
// know to look at pending macro tasks.
@@ -463,7 +483,8 @@ describe('MdCheckbox', () => {
463483
[checked]="isChecked"
464484
[indeterminate]="isIndeterminate"
465485
[disabled]="isDisabled"
466-
(change)="changeCount = changeCount + 1">
486+
(change)="changeCount = changeCount + 1"
487+
(click)="onCheckboxClick($event)">
467488
Simple checkbox
468489
</md-checkbox>
469490
</div>`
@@ -476,6 +497,8 @@ class SingleCheckbox {
476497
parentElementClicked: boolean = false;
477498
parentElementKeyedUp: boolean = false;
478499
lastKeydownEvent: Event = null;
500+
501+
onCheckboxClick(event: Event) {}
479502
}
480503

481504
/** Simple component for testing an MdCheckbox with ngModel and ngControl. */

src/components/checkbox/checkbox.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,18 @@ export class MdCheckbox implements AfterContentInit, ControlValueAccessor {
278278
}
279279
}
280280

281+
/** @internal */
282+
onInputClick(event: Event) {
283+
// We have to stop propagation for click events on the visual hidden input element.
284+
// By default, when a user clicks on a label element, a generated click event will be
285+
// dispatched on the associated input element. Since we are using a label element as our
286+
// root container, the click event on the `checkbox` will be executed twice.
287+
// The real click event will bubble up, and the generated click event also tries to bubble up.
288+
// This will lead to multiple click events.
289+
// Preventing bubbling for the second event will solve that issue.
290+
event.stopPropagation();
291+
}
292+
281293
private _getAnimationClassForCheckStateTransition(
282294
oldState: TransitionCheckState, newState: TransitionCheckState): string {
283295
var animSuffix: string;

src/components/slide-toggle/slide-toggle.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
[attr.aria-labelledby]="ariaLabelledby"
1818
(blur)="onInputBlur()"
1919
(focus)="onInputFocus()"
20-
(change)="onChangeEvent($event)">
20+
(change)="onChangeEvent($event)"
21+
(click)="onInputClick($event)">
2122
</div>
2223
<span class="md-slide-toggle-content">
2324
<ng-content></ng-content>

src/components/slide-toggle/slide-toggle.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,26 @@ describe('MdSlideToggle', () => {
9797
expect(slideToggle.checked).toBe(true);
9898
});
9999

100+
it('should not trigger the click event multiple times', () => {
101+
// By default, when clicking on a label element, a generated click will be dispatched
102+
// on the associated input element.
103+
// Since we're using a label element and a visual hidden input, this behavior can led
104+
// to an issue, where the click events on the slide-toggle are getting executed twice.
105+
106+
spyOn(testComponent, 'onSlideClick');
107+
108+
expect(slideToggle.checked).toBe(false);
109+
expect(slideToggleElement.classList).not.toContain('md-checked');
110+
111+
labelElement.click();
112+
fixture.detectChanges();
113+
114+
expect(slideToggleElement.classList).toContain('md-checked');
115+
expect(slideToggle.checked).toBe(true);
116+
117+
expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1);
118+
});
119+
100120
it('should add a suffix to the inputs id', () => {
101121
testComponent.slideId = 'myId';
102122
fixture.detectChanges();
@@ -268,7 +288,8 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void
268288
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled" [color]="slideColor"
269289
[id]="slideId" [checked]="slideChecked" [name]="slideName"
270290
[aria-label]="slideLabel" [ariaLabel]="slideLabel"
271-
[ariaLabelledby]="slideLabelledBy" (change)="lastEvent = $event">
291+
[ariaLabelledby]="slideLabelledBy" (change)="lastEvent = $event"
292+
(click)="onSlideClick($event)">
272293
<span>Test Slide Toggle</span>
273294
</md-slide-toggle>
274295
`,
@@ -284,4 +305,6 @@ class SlideToggleTestApp {
284305
slideLabel: string;
285306
slideLabelledBy: string;
286307
lastEvent: MdSlideToggleChange;
308+
309+
onSlideClick(event: Event) {}
287310
}

src/components/slide-toggle/slide-toggle.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ let nextId = 0;
3838
'[class.md-disabled]': 'disabled',
3939
// This md-slide-toggle prefix will change, once the temporary ripple is removed.
4040
'[class.md-slide-toggle-focused]': '_hasFocus',
41-
'(click)': 'onTouched()',
4241
'(mousedown)': 'setMousedown()'
4342
},
4443
templateUrl: 'slide-toggle.html',
@@ -92,6 +91,20 @@ export class MdSlideToggle implements ControlValueAccessor {
9291
}
9392
}
9493

94+
/** @internal */
95+
onInputClick(event: Event) {
96+
this.onTouched();
97+
98+
// We have to stop propagation for click events on the visual hidden input element.
99+
// By default, when a user clicks on a label element, a generated click event will be
100+
// dispatched on the associated input element. Since we are using a label element as our
101+
// root container, the click event on the `slide-toggle` will be executed twice.
102+
// The real click event will bubble up, and the generated click event also tries to bubble up.
103+
// This will lead to multiple click events.
104+
// Preventing bubbling for the second event will solve that issue.
105+
event.stopPropagation();
106+
}
107+
95108
/** @internal */
96109
setMousedown() {
97110
// We only *show* the focus style when focus has come to the button via the keyboard.

0 commit comments

Comments
 (0)