diff --git a/src/lib/checkbox/checkbox.html b/src/lib/checkbox/checkbox.html index 8c979eff04ef..e2ebc48b0249 100644 --- a/src/lib/checkbox/checkbox.html +++ b/src/lib/checkbox/checkbox.html @@ -12,11 +12,11 @@ [indeterminate]="indeterminate" [attr.aria-label]="ariaLabel" [attr.aria-labelledby]="ariaLabelledby" - (blur)="_onInputBlur()" (change)="_onInteractionEvent($event)" (click)="_onInputClick($event)"> -
diff --git a/src/lib/checkbox/checkbox.spec.ts b/src/lib/checkbox/checkbox.spec.ts index 10d86e54f58d..180a4003fa7a 100644 --- a/src/lib/checkbox/checkbox.spec.ts +++ b/src/lib/checkbox/checkbox.spec.ts @@ -359,23 +359,6 @@ describe('MdCheckbox', () => { .toBe(0, 'Expected no ripple after element is blurred.'); })); - it('should show a ripple when focused programmatically', fakeAsync(() => { - expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) - .toBe(0, 'Expected no ripples to be present.'); - - dispatchFakeEvent(inputElement, 'focus'); - tick(RIPPLE_FADE_IN_DURATION); - - expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) - .toBe(1, 'Expected focus ripple to be present.'); - - dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur'); - tick(RIPPLE_FADE_OUT_DURATION); - - expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) - .toBe(0, 'Expected focus ripple to be removed.'); - })); - describe('ripple elements', () => { it('should show ripples on label mousedown', () => { @@ -387,30 +370,41 @@ describe('MdCheckbox', () => { expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1); }); - it('should not have a ripple when disabled', () => { - let rippleElement = checkboxNativeElement.querySelector('[md-ripple]'); - expect(rippleElement).toBeTruthy('Expected an enabled checkbox to have a ripple'); - + it('should not show ripples when disabled', () => { testComponent.isDisabled = true; fixture.detectChanges(); - rippleElement = checkboxNativeElement.querySelector('[md-ripple]'); - expect(rippleElement).toBeFalsy('Expected a disabled checkbox not to have a ripple'); + dispatchFakeEvent(labelElement, 'mousedown'); + dispatchFakeEvent(labelElement, 'mouseup'); + + expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(0); + + testComponent.isDisabled = false; + fixture.detectChanges(); + + dispatchFakeEvent(labelElement, 'mousedown'); + dispatchFakeEvent(labelElement, 'mouseup'); + + expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1); }); - it('should remove ripple if mdRippleDisabled input is set', async(() => { + it('should remove ripple if mdRippleDisabled input is set', () => { testComponent.disableRipple = true; fixture.detectChanges(); - expect(checkboxNativeElement.querySelectorAll('[md-ripple]').length) - .toBe(0, 'Expect no [md-ripple] in checkbox'); + dispatchFakeEvent(labelElement, 'mousedown'); + dispatchFakeEvent(labelElement, 'mouseup'); + + expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(0); testComponent.disableRipple = false; fixture.detectChanges(); - expect(checkboxNativeElement.querySelectorAll('[md-ripple]').length) - .toBe(1, 'Expect [md-ripple] in checkbox'); - })); + dispatchFakeEvent(labelElement, 'mousedown'); + dispatchFakeEvent(labelElement, 'mouseup'); + + expect(checkboxNativeElement.querySelectorAll('.mat-ripple-element').length).toBe(1); + }); }); describe('color behaviour', () => { diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index ae9e0b00f09b..e7a69a412169 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -15,11 +15,11 @@ import { } from '@angular/core'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {Subscription} from 'rxjs/Subscription'; import { MdRipple, RippleRef, FocusOriginMonitor, + FocusOrigin, } from '../core'; @@ -183,10 +183,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro private _controlValueAccessorChangeFn: (value: any) => void = (value) => {}; /** Reference to the focused state ripple. */ - private _focusedRipple: RippleRef; - - /** Reference to the focus origin monitor subscription. */ - private _focusedSubscription: Subscription; + private _focusRipple: RippleRef; constructor(private _renderer: Renderer, private _elementRef: ElementRef, @@ -196,13 +193,9 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro } ngAfterViewInit() { - this._focusedSubscription = this._focusOriginMonitor + this._focusOriginMonitor .monitor(this._inputElement.nativeElement, this._renderer, false) - .subscribe(focusOrigin => { - if (!this._focusedRipple && (focusOrigin === 'keyboard' || focusOrigin === 'program')) { - this._focusedRipple = this._ripple.launch(0, 0, { persistent: true, centered: true }); - } - }); + .subscribe(focusOrigin => this._onInputFocusChange(focusOrigin)); } ngOnDestroy() { @@ -343,10 +336,14 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro this.change.emit(event); } - /** Informs the component when we lose focus in order to style accordingly */ - _onInputBlur() { - this._removeFocusedRipple(); - this.onTouched(); + /** Function is called whenever the focus changes for the input element. */ + private _onInputFocusChange(focusOrigin: FocusOrigin) { + if (!this._focusRipple && focusOrigin === 'keyboard') { + this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true}); + } else if (!focusOrigin) { + this._removeFocusRipple(); + this.onTouched(); + } } /** Toggles the `checked` state of the checkbox. */ @@ -371,7 +368,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro // Preventing bubbling for the second event will solve that issue. event.stopPropagation(); - this._removeFocusedRipple(); + this._removeFocusRipple(); if (!this.disabled) { this.toggle(); @@ -387,7 +384,7 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro /** Focuses the checkbox. */ focus(): void { - this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program'); + this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard'); } _onInteractionEvent(event: Event) { @@ -429,11 +426,11 @@ export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestro return `mat-checkbox-anim-${animSuffix}`; } - /** Fades out the focused state ripple. */ - private _removeFocusedRipple(): void { - if (this._focusedRipple) { - this._focusedRipple.fadeOut(); - this._focusedRipple = null; + /** Fades out the focus state ripple. */ + private _removeFocusRipple(): void { + if (this._focusRipple) { + this._focusRipple.fadeOut(); + this._focusRipple = null; } } } diff --git a/src/lib/radio/radio.html b/src/lib/radio/radio.html index 02423b631eec..ca84be26b665 100644 --- a/src/lib/radio/radio.html +++ b/src/lib/radio/radio.html @@ -5,8 +5,9 @@
-
@@ -18,7 +19,6 @@ [attr.aria-label]="ariaLabel" [attr.aria-labelledby]="ariaLabelledby" (change)="_onInputChange($event)" - (blur)="_onInputBlur()" (click)="_onInputClick($event)"> diff --git a/src/lib/radio/radio.spec.ts b/src/lib/radio/radio.spec.ts index e1cc41333fc5..c977f51e6e77 100644 --- a/src/lib/radio/radio.spec.ts +++ b/src/lib/radio/radio.spec.ts @@ -228,33 +228,47 @@ describe('MdRadio', () => { expect(radioInstances.every(radio => !radio.checked)).toBe(true); }); - it('should not have a ripple on disabled radio buttons', () => { - let rippleElement = radioNativeElements[0].querySelector('[md-ripple]'); - expect(rippleElement).toBeTruthy('Expected an enabled radio button to have a ripple'); - + it('should not show ripples on disabled radio buttons', () => { radioInstances[0].disabled = true; fixture.detectChanges(); - rippleElement = radioNativeElements[0].querySelector('[md-ripple]'); - expect(rippleElement).toBeFalsy('Expected a disabled radio button not to have a ripple'); + dispatchFakeEvent(radioLabelElements[0], 'mousedown'); + dispatchFakeEvent(radioLabelElements[0], 'mouseup'); + + expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length) + .toBe(0, 'Expected a disabled radio button to not show ripples'); + + radioInstances[0].disabled = false; + fixture.detectChanges(); + + dispatchFakeEvent(radioLabelElements[0], 'mousedown'); + dispatchFakeEvent(radioLabelElements[0], 'mouseup'); + + expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length) + .toBe(1, 'Expected an enabled radio button to show ripples'); }); - it('should remove ripple if mdRippleDisabled input is set', async(() => { + it('should not show ripples if mdRippleDisabled input is set', () => { + testComponent.disableRipple = true; fixture.detectChanges(); - for (let radioNativeElement of radioNativeElements) - { - expect(radioNativeElement.querySelectorAll('[md-ripple]').length) - .toBe(1, 'Expect [md-ripple] in radio buttons'); + + for (let radioLabel of radioLabelElements) { + dispatchFakeEvent(radioLabel, 'mousedown'); + dispatchFakeEvent(radioLabel, 'mouseup'); + + expect(radioLabel.querySelectorAll('.mat-ripple-element').length).toBe(0); } - testComponent.disableRipple = true; + testComponent.disableRipple = false; fixture.detectChanges(); - for (let radioNativeElement of radioNativeElements) - { - expect(radioNativeElement.querySelectorAll('[md-ripple]').length) - .toBe(0, 'Expect no [md-ripple] in radio buttons'); + + for (let radioLabel of radioLabelElements) { + dispatchFakeEvent(radioLabel, 'mousedown'); + dispatchFakeEvent(radioLabel, 'mouseup'); + + expect(radioLabel.querySelectorAll('.mat-ripple-element').length).toBe(1); } - })); + }); it(`should update the group's selected radio to null when unchecking that radio programmatically`, () => { diff --git a/src/lib/radio/radio.ts b/src/lib/radio/radio.ts index 7e72e6c2d032..a8dab7a488a0 100644 --- a/src/lib/radio/radio.ts +++ b/src/lib/radio/radio.ts @@ -23,9 +23,9 @@ import { UniqueSelectionDispatcher, MdRipple, FocusOriginMonitor, + FocusOrigin, } from '../core'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {Subscription} from 'rxjs/Subscription'; /** @@ -405,11 +405,8 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy { /** The child ripple instance. */ @ViewChild(MdRipple) _ripple: MdRipple; - /** Stream of focus event from the focus origin monitor. */ - private _focusOriginMonitorSubscription: Subscription; - /** Reference to the current focus ripple. */ - private _focusedRippleRef: RippleRef; + private _focusRipple: RippleRef; /** The native `` element */ @ViewChild('input') _inputElement: ElementRef; @@ -446,13 +443,9 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy { } ngAfterViewInit() { - this._focusOriginMonitorSubscription = this._focusOriginMonitor + this._focusOriginMonitor .monitor(this._inputElement.nativeElement, this._renderer, false) - .subscribe(focusOrigin => { - if (focusOrigin === 'keyboard' && !this._focusedRippleRef) { - this._focusedRippleRef = this._ripple.launch(0, 0, { persistent: true, centered: true }); - } - }); + .subscribe(focusOrigin => this._onInputFocusChange(focusOrigin)); } ngOnDestroy() { @@ -471,17 +464,6 @@ export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy { return this.disableRipple || this.disabled; } - _onInputBlur() { - if (this._focusedRippleRef) { - this._focusedRippleRef.fadeOut(); - this._focusedRippleRef = null; - } - - if (this.radioGroup) { - this.radioGroup._touch(); - } - } - _onInputClick(event: Event) { // We have to stop propagation for click events on the visual hidden input element. // 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 { } } + /** Function is called whenever the focus changes for the input element. */ + private _onInputFocusChange(focusOrigin: FocusOrigin) { + if (!this._focusRipple && focusOrigin === 'keyboard') { + this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true}); + } else if (!focusOrigin) { + if (this.radioGroup) { + this.radioGroup._touch(); + } + + if (this._focusRipple) { + this._focusRipple.fadeOut(); + this._focusRipple = null; + } + } + } + } diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts index 864eba4288db..21927436e996 100644 --- a/src/lib/slide-toggle/slide-toggle.ts +++ b/src/lib/slide-toggle/slide-toggle.ts @@ -204,7 +204,7 @@ export class MdSlideToggle implements OnDestroy, AfterContentInit, ControlValueA /** Focuses the slide-toggle. */ focus() { - this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program'); + this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard'); } /** Whether the slide-toggle is checked. */