diff --git a/src/lib/radio/radio.html b/src/lib/radio/radio.html
index d4293cbee92d..02423b631eec 100644
--- a/src/lib/radio/radio.html
+++ b/src/lib/radio/radio.html
@@ -18,7 +18,6 @@
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby"
(change)="_onInputChange($event)"
- (focus)="_onInputFocus()"
(blur)="_onInputBlur()"
(click)="_onInputClick($event)">
diff --git a/src/lib/radio/radio.spec.ts b/src/lib/radio/radio.spec.ts
index 92d0ee86d414..eaccd2ca4099 100644
--- a/src/lib/radio/radio.spec.ts
+++ b/src/lib/radio/radio.spec.ts
@@ -6,9 +6,21 @@ import {MdRadioGroup, MdRadioButton, MdRadioChange, MdRadioModule} from './radio
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
+import {FocusOriginMonitor, FocusOrigin} from '../core';
+import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer';
+import {Subject} from 'rxjs/Subject';
describe('MdRadio', () => {
+ let fakeFocusOriginMonitorStream = new Subject();
+ let fakeFocusOriginMonitor = {
+ monitor: () => fakeFocusOriginMonitorStream.asObservable(),
+ unmonitor: () => {},
+ focusVia: (element: HTMLElement, renderer: any, origin: FocusOrigin) => {
+ element.focus();
+ fakeFocusOriginMonitorStream.next(origin);
+ }
+ };
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -21,6 +33,7 @@ describe('MdRadio', () => {
],
providers: [
{provide: ViewportRuler, useClass: FakeViewportRuler},
+ {provide: FocusOriginMonitor, useValue: fakeFocusOriginMonitor}
]
});
@@ -177,37 +190,22 @@ describe('MdRadio', () => {
expect(changeSpy).toHaveBeenCalledTimes(1);
});
- // TODO(jelbourn): test this in an e2e test with *real* focus, rather than faking
- // a focus / blur event.
- it('should focus individual radio buttons', () => {
- let nativeRadioInput = radioNativeElements[0].querySelector('input');
-
- expect(nativeRadioInput.classList).not.toContain('mat-radio-focused');
-
- dispatchFakeEvent(nativeRadioInput, 'focus');
- fixture.detectChanges();
+ it('should show a ripple when focusing via the keyboard', fakeAsync(() => {
+ expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripples on init.');
- expect(radioNativeElements[0].classList).toContain('mat-radio-focused');
+ fakeFocusOriginMonitorStream.next('keyboard');
+ tick(RIPPLE_FADE_IN_DURATION);
- dispatchFakeEvent(nativeRadioInput, 'blur');
- fixture.detectChanges();
-
- expect(radioNativeElements[0].classList).not.toContain('mat-radio-focused');
- });
+ expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length)
+ .toBe(1, 'Expected one ripple after keyboard focus.');
- it('should focus individual radio buttons', () => {
- let nativeRadioInput = radioNativeElements[0].querySelector('input');
+ dispatchFakeEvent(radioNativeElements[0].querySelector('input'), 'blur');
+ tick(RIPPLE_FADE_OUT_DURATION);
- radioInstances[0].focus();
- fixture.detectChanges();
-
- expect(radioNativeElements[0].classList).toContain('mat-radio-focused');
-
- dispatchFakeEvent(nativeRadioInput, 'blur');
- fixture.detectChanges();
-
- expect(radioNativeElements[0].classList).not.toContain('mat-radio-focused');
- });
+ expect(radioNativeElements[0].querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripples on blur.');
+ }));
it('should update the group and radios when updating the group value', () => {
expect(groupInstance.value).toBeFalsy();
diff --git a/src/lib/radio/radio.ts b/src/lib/radio/radio.ts
index 4e0809eb0bf3..1ccc01990fd8 100644
--- a/src/lib/radio/radio.ts
+++ b/src/lib/radio/radio.ts
@@ -6,7 +6,6 @@ import {
ElementRef,
Renderer,
EventEmitter,
- HostBinding,
Input,
OnInit,
Optional,
@@ -17,17 +16,23 @@ import {
NgModule,
ModuleWithProviders,
ViewChild,
+ OnDestroy,
+ AfterViewInit,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
import {
MdRippleModule,
+ RippleRef,
UniqueSelectionDispatcher,
CompatibilityModule,
UNIQUE_SELECTION_DISPATCHER_PROVIDER,
+ MdRipple,
+ FocusOriginMonitor,
} from '../core';
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
import {VIEWPORT_RULER_PROVIDER} from '../core/overlay/position/viewport-ruler';
+import {Subscription} from 'rxjs/Subscription';
/**
@@ -265,24 +270,21 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
encapsulation: ViewEncapsulation.None,
host: {
'[class.mat-radio-button]': 'true',
+ '[class.mat-radio-checked]': 'checked',
+ '[class.mat-radio-disabled]': 'disabled',
+ '[attr.id]': 'id',
}
})
-export class MdRadioButton implements OnInit {
-
- @HostBinding('class.mat-radio-focused')
- _isFocused: boolean;
+export class MdRadioButton implements OnInit, AfterViewInit, OnDestroy {
/** Whether this radio is checked. */
private _checked: boolean = false;
/** The unique ID for the radio button. */
- @HostBinding('id')
- @Input()
- id: string = `md-radio-${_uniqueIdCounter++}`;
+ @Input() id: string = `md-radio-${_uniqueIdCounter++}`;
/** Analog to HTML 'name' attribute used to group radios for unique selection. */
- @Input()
- name: string;
+ @Input() name: string;
/** Used to set the 'aria-label' attribute on the underlying input element. */
@Input('aria-label') ariaLabel: string;
@@ -299,6 +301,15 @@ export class MdRadioButton implements OnInit {
/** Whether the ripple effect on click should be disabled. */
private _disableRipple: boolean;
+ /** 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;
+
/** The parent radio group. May or may not be present. */
radioGroup: MdRadioGroup;
@@ -321,6 +332,7 @@ export class MdRadioButton implements OnInit {
constructor(@Optional() radioGroup: MdRadioGroup,
private _elementRef: ElementRef,
private _renderer: Renderer,
+ private _focusOriginMonitor: FocusOriginMonitor,
public radioDispatcher: UniqueSelectionDispatcher) {
// Assertions. Ideally these should be stripped out by the compiler.
// TODO(jelbourn): Assert that there's no name binding AND a parent radio group.
@@ -340,7 +352,6 @@ export class MdRadioButton implements OnInit {
}
/** Whether this radio button is checked. */
- @HostBinding('class.mat-radio-checked')
@Input()
get checked(): boolean {
return this._checked;
@@ -415,7 +426,6 @@ export class MdRadioButton implements OnInit {
}
/** Whether the radio button is disabled. */
- @HostBinding('class.mat-radio-disabled')
@Input()
get disabled(): boolean {
return this._disabled || (this.radioGroup != null && this.radioGroup.disabled);
@@ -435,6 +445,25 @@ export class MdRadioButton implements OnInit {
}
}
+ ngAfterViewInit() {
+ this._focusOriginMonitorSubscription = 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 });
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this._focusOriginMonitor.unmonitor(this._inputElement.nativeElement);
+
+ if (this._focusOriginMonitorSubscription) {
+ this._focusOriginMonitorSubscription.unsubscribe();
+ this._focusOriginMonitorSubscription = null;
+ }
+ }
+
/** Dispatch change event with current value. */
private _emitChangeEvent(): void {
let event = new MdRadioChange();
@@ -447,23 +476,16 @@ export class MdRadioButton implements OnInit {
return this.disableRipple || this.disabled;
}
- /**
- * We use a hidden native input field to handle changes to focus state via keyboard navigation,
- * with visual rendering done separately. The native element is kept in sync with the overall
- * state of the component.
- */
- _onInputFocus() {
- this._isFocused = true;
- }
-
/** Focuses the radio button. */
focus(): void {
- this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus');
- this._onInputFocus();
+ this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard');
}
_onInputBlur() {
- this._isFocused = false;
+ if (this._focusedRippleRef) {
+ this._focusedRippleRef.fadeOut();
+ this._focusedRippleRef = null;
+ }
if (this.radioGroup) {
this.radioGroup._touch();
@@ -503,13 +525,14 @@ export class MdRadioButton implements OnInit {
}
}
}
+
}
@NgModule({
imports: [CommonModule, MdRippleModule, CompatibilityModule],
exports: [MdRadioGroup, MdRadioButton, CompatibilityModule],
- providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER, VIEWPORT_RULER_PROVIDER],
+ providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER, VIEWPORT_RULER_PROVIDER, FocusOriginMonitor],
declarations: [MdRadioGroup, MdRadioButton],
})
export class MdRadioModule {