Skip to content

Commit e22b55e

Browse files
devversionjelbourn
authored andcommitted
fix(checkbox, radio): ripple error on focus event (#3869)
* The checkbox and radio disable the ripples using a `*ngFor` check. This causes the `ViewChildren` instance to turn null. The reference is required for the focus indicators. * If ripples are disabled there will be still ripples on click. This is due to the fact that label elements redirect focus to the underlying input element. The `focusOrigin` is therefore `program` and a focus ripple will show up. * Tests had to be adjusted because the `[md-ripple]` element won't be removed anymore. Therefore the tests now confirm that no ripples are showing up. Fixes #3856.
1 parent 7f65f31 commit e22b55e

File tree

7 files changed

+98
-95
lines changed

7 files changed

+98
-95
lines changed

src/lib/checkbox/checkbox.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
[indeterminate]="indeterminate"
1313
[attr.aria-label]="ariaLabel"
1414
[attr.aria-labelledby]="ariaLabelledby"
15-
(blur)="_onInputBlur()"
1615
(change)="_onInteractionEvent($event)"
1716
(click)="_onInputClick($event)">
18-
<div md-ripple *ngIf="!_isRippleDisabled()" class="mat-checkbox-ripple"
17+
<div md-ripple class="mat-checkbox-ripple"
1918
[mdRippleTrigger]="label"
19+
[mdRippleDisabled]="_isRippleDisabled()"
2020
[mdRippleCentered]="true"></div>
2121
<div class="mat-checkbox-frame"></div>
2222
<div class="mat-checkbox-background">

src/lib/checkbox/checkbox.spec.ts

+23-29
Original file line numberDiff line numberDiff line change
@@ -359,23 +359,6 @@ describe('MdCheckbox', () => {
359359
.toBe(0, 'Expected no ripple after element is blurred.');
360360
}));
361361

362-
it('should show a ripple when focused programmatically', fakeAsync(() => {
363-
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
364-
.toBe(0, 'Expected no ripples to be present.');
365-
366-
dispatchFakeEvent(inputElement, 'focus');
367-
tick(RIPPLE_FADE_IN_DURATION);
368-
369-
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
370-
.toBe(1, 'Expected focus ripple to be present.');
371-
372-
dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur');
373-
tick(RIPPLE_FADE_OUT_DURATION);
374-
375-
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
376-
.toBe(0, 'Expected focus ripple to be removed.');
377-
}));
378-
379362
describe('ripple elements', () => {
380363

381364
it('should show ripples on label mousedown', () => {
@@ -387,30 +370,41 @@ describe('MdCheckbox', () => {
387370
expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1);
388371
});
389372

390-
it('should not have a ripple when disabled', () => {
391-
let rippleElement = checkboxNativeElement.querySelector('[md-ripple]');
392-
expect(rippleElement).toBeTruthy('Expected an enabled checkbox to have a ripple');
393-
373+
it('should not show ripples when disabled', () => {
394374
testComponent.isDisabled = true;
395375
fixture.detectChanges();
396376

397-
rippleElement = checkboxNativeElement.querySelector('[md-ripple]');
398-
expect(rippleElement).toBeFalsy('Expected a disabled checkbox not to have a ripple');
377+
dispatchFakeEvent(labelElement, 'mousedown');
378+
dispatchFakeEvent(labelElement, 'mouseup');
379+
380+
expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(0);
381+
382+
testComponent.isDisabled = false;
383+
fixture.detectChanges();
384+
385+
dispatchFakeEvent(labelElement, 'mousedown');
386+
dispatchFakeEvent(labelElement, 'mouseup');
387+
388+
expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1);
399389
});
400390

401-
it('should remove ripple if mdRippleDisabled input is set', async(() => {
391+
it('should remove ripple if mdRippleDisabled input is set', () => {
402392
testComponent.disableRipple = true;
403393
fixture.detectChanges();
404394

405-
expect(checkboxNativeElement.querySelectorAll('[md-ripple]').length)
406-
.toBe(0, 'Expect no [md-ripple] in checkbox');
395+
dispatchFakeEvent(labelElement, 'mousedown');
396+
dispatchFakeEvent(labelElement, 'mouseup');
397+
398+
expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(0);
407399

408400
testComponent.disableRipple = false;
409401
fixture.detectChanges();
410402

411-
expect(checkboxNativeElement.querySelectorAll('[md-ripple]').length)
412-
.toBe(1, 'Expect [md-ripple] in checkbox');
413-
}));
403+
dispatchFakeEvent(labelElement, 'mousedown');
404+
dispatchFakeEvent(labelElement, 'mouseup');
405+
406+
expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1);
407+
});
414408
});
415409

416410
describe('color behaviour', () => {

src/lib/checkbox/checkbox.ts

+19-22
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import {
1515
} from '@angular/core';
1616
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1717
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
18-
import {Subscription} from 'rxjs/Subscription';
1918
import {
2019
MdRipple,
2120
RippleRef,
2221
FocusOriginMonitor,
22+
FocusOrigin,
2323
} from '../core';
2424

2525

@@ -183,10 +183,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
183183
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
184184

185185
/** Reference to the focused state ripple. */
186-
private _focusedRipple: RippleRef;
187-
188-
/** Reference to the focus origin monitor subscription. */
189-
private _focusedSubscription: Subscription;
186+
private _focusRipple: RippleRef;
190187

191188
constructor(private _renderer: Renderer,
192189
private _elementRef: ElementRef,
@@ -196,13 +193,9 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
196193
}
197194

198195
ngAfterViewInit() {
199-
this._focusedSubscription = this._focusOriginMonitor
196+
this._focusOriginMonitor
200197
.monitor(this._inputElement.nativeElement, this._renderer, false)
201-
.subscribe(focusOrigin => {
202-
if (!this._focusedRipple && (focusOrigin === 'keyboard' || focusOrigin === 'program')) {
203-
this._focusedRipple = this._ripple.launch(0, 0, { persistent: true, centered: true });
204-
}
205-
});
198+
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
206199
}
207200

208201
ngOnDestroy() {
@@ -343,10 +336,14 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
343336
this.change.emit(event);
344337
}
345338

346-
/** Informs the component when we lose focus in order to style accordingly */
347-
_onInputBlur() {
348-
this._removeFocusedRipple();
349-
this.onTouched();
339+
/** Function is called whenever the focus changes for the input element. */
340+
private _onInputFocusChange(focusOrigin: FocusOrigin) {
341+
if (!this._focusRipple && focusOrigin === 'keyboard') {
342+
this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true});
343+
} else if (!focusOrigin) {
344+
this._removeFocusRipple();
345+
this.onTouched();
346+
}
350347
}
351348

352349
/** Toggles the `checked` state of the checkbox. */
@@ -371,7 +368,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
371368
// Preventing bubbling for the second event will solve that issue.
372369
event.stopPropagation();
373370

374-
this._removeFocusedRipple();
371+
this._removeFocusRipple();
375372

376373
if (!this.disabled) {
377374
this.toggle();
@@ -387,7 +384,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
387384

388385
/** Focuses the checkbox. */
389386
focus(): void {
390-
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program');
387+
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard');
391388
}
392389

393390
_onInteractionEvent(event: Event) {
@@ -429,11 +426,11 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro
429426
return `mat-checkbox-anim-${animSuffix}`;
430427
}
431428

432-
/** Fades out the focused state ripple. */
433-
private _removeFocusedRipple(): void {
434-
if (this._focusedRipple) {
435-
this._focusedRipple.fadeOut();
436-
this._focusedRipple = null;
429+
/** Fades out the focus state ripple. */
430+
private _removeFocusRipple(): void {
431+
if (this._focusRipple) {
432+
this._focusRipple.fadeOut();
433+
this._focusRipple = null;
437434
}
438435
}
439436
}

src/lib/radio/radio.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
<div class="mat-radio-container">
66
<div class="mat-radio-outer-circle"></div>
77
<div class="mat-radio-inner-circle"></div>
8-
<div md-ripple *ngIf="!_isRippleDisabled()" class="mat-radio-ripple"
8+
<div md-ripple class="mat-radio-ripple"
99
[mdRippleTrigger]="label"
10+
[mdRippleDisabled]="_isRippleDisabled()"
1011
[mdRippleCentered]="true"></div>
1112
</div>
1213

@@ -18,7 +19,6 @@
1819
[attr.aria-label]="ariaLabel"
1920
[attr.aria-labelledby]="ariaLabelledby"
2021
(change)="_onInputChange($event)"
21-
(blur)="_onInputBlur()"
2222
(click)="_onInputClick($event)">
2323

2424
<!-- The label content for radio control. -->

src/lib/radio/radio.spec.ts

+31-17
Original file line numberDiff line numberDiff line change
@@ -228,33 +228,47 @@ describe('MdRadio', () => {
228228
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
229229
});
230230

231-
it('should not have a ripple on disabled radio buttons', () => {
232-
let rippleElement = radioNativeElements[0].querySelector('[md-ripple]');
233-
expect(rippleElement).toBeTruthy('Expected an enabled radio button to have a ripple');
234-
231+
it('should not show ripples on disabled radio buttons', () => {
235232
radioInstances[0].disabled = true;
236233
fixture.detectChanges();
237234

238-
rippleElement = radioNativeElements[0].querySelector('[md-ripple]');
239-
expect(rippleElement).toBeFalsy('Expected a disabled radio button not to have a ripple');
235+
dispatchFakeEvent(radioLabelElements[0], 'mousedown');
236+
dispatchFakeEvent(radioLabelElements[0], 'mouseup');
237+
238+
expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length)
239+
.toBe(0, 'Expected a disabled radio button to not show ripples');
240+
241+
radioInstances[0].disabled = false;
242+
fixture.detectChanges();
243+
244+
dispatchFakeEvent(radioLabelElements[0], 'mousedown');
245+
dispatchFakeEvent(radioLabelElements[0], 'mouseup');
246+
247+
expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length)
248+
.toBe(1, 'Expected an enabled radio button to show ripples');
240249
});
241250

242-
it('should remove ripple if mdRippleDisabled input is set', async(() => {
251+
it('should not show ripples if mdRippleDisabled input is set', () => {
252+
testComponent.disableRipple = true;
243253
fixture.detectChanges();
244-
for (let radioNativeElement of radioNativeElements)
245-
{
246-
expect(radioNativeElement.querySelectorAll('[md-ripple]').length)
247-
.toBe(1, 'Expect [md-ripple] in radio buttons');
254+
255+
for (let radioLabel of radioLabelElements) {
256+
dispatchFakeEvent(radioLabel, 'mousedown');
257+
dispatchFakeEvent(radioLabel, 'mouseup');
258+
259+
expect(radioLabel.querySelectorAll('.mat-ripple-element').length).toBe(0);
248260
}
249261

250-
testComponent.disableRipple = true;
262+
testComponent.disableRipple = false;
251263
fixture.detectChanges();
252-
for (let radioNativeElement of radioNativeElements)
253-
{
254-
expect(radioNativeElement.querySelectorAll('[md-ripple]').length)
255-
.toBe(0, 'Expect no [md-ripple] in radio buttons');
264+
265+
for (let radioLabel of radioLabelElements) {
266+
dispatchFakeEvent(radioLabel, 'mousedown');
267+
dispatchFakeEvent(radioLabel, 'mouseup');
268+
269+
expect(radioLabel.querySelectorAll('.mat-ripple-element').length).toBe(1);
256270
}
257-
}));
271+
});
258272

259273
it(`should update the group's selected radio to null when unchecking that radio
260274
programmatically`, () => {

src/lib/radio/radio.ts

+20-22
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
UniqueSelectionDispatcher,
2424
MdRipple,
2525
FocusOriginMonitor,
26+
FocusOrigin,
2627
} from '../core';
2728
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
28-
import {Subscription} from 'rxjs/Subscription';
2929

3030

3131
/**
@@ -405,11 +405,8 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy {
405405
/** The child ripple instance. */
406406
@ViewChild(MdRipple) _ripple: MdRipple;
407407

408-
/** Stream of focus event from the focus origin monitor. */
409-
private _focusOriginMonitorSubscription: Subscription;
410-
411408
/** Reference to the current focus ripple. */
412-
private _focusedRippleRef: RippleRef;
409+
private _focusRipple: RippleRef;
413410

414411
/** The native `<input type=radio>` element */
415412
@ViewChild('input') _inputElement: ElementRef;
@@ -446,13 +443,9 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy {
446443
}
447444

448445
ngAfterViewInit() {
449-
this._focusOriginMonitorSubscription = this._focusOriginMonitor
446+
this._focusOriginMonitor
450447
.monitor(this._inputElement.nativeElement, this._renderer, false)
451-
.subscribe(focusOrigin => {
452-
if (focusOrigin === 'keyboard' && !this._focusedRippleRef) {
453-
this._focusedRippleRef = this._ripple.launch(0, 0, { persistent: true, centered: true });
454-
}
455-
});
448+
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
456449
}
457450

458451
ngOnDestroy() {
@@ -471,17 +464,6 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy {
471464
return this.disableRipple || this.disabled;
472465
}
473466

474-
_onInputBlur() {
475-
if (this._focusedRippleRef) {
476-
this._focusedRippleRef.fadeOut();
477-
this._focusedRippleRef = null;
478-
}
479-
480-
if (this.radioGroup) {
481-
this.radioGroup._touch();
482-
}
483-
}
484-
485467
_onInputClick(event: Event) {
486468
// We have to stop propagation for click events on the visual hidden input element.
487469
// By default, when a user clicks on a label element, a generated click event will be
@@ -516,4 +498,20 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy {
516498
}
517499
}
518500

501+
/** Function is called whenever the focus changes for the input element. */
502+
private _onInputFocusChange(focusOrigin: FocusOrigin) {
503+
if (!this._focusRipple && focusOrigin === 'keyboard') {
504+
this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true});
505+
} else if (!focusOrigin) {
506+
if (this.radioGroup) {
507+
this.radioGroup._touch();
508+
}
509+
510+
if (this._focusRipple) {
511+
this._focusRipple.fadeOut();
512+
this._focusRipple = null;
513+
}
514+
}
515+
}
516+
519517
}

src/lib/slide-toggle/slide-toggle.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export class MdSlideToggle implements OnDestroy, AfterContentInit, ControlValueA
204204

205205
/** Focuses the slide-toggle. */
206206
focus() {
207-
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program');
207+
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard');
208208
}
209209

210210
/** Whether the slide-toggle is checked. */

0 commit comments

Comments
 (0)