feat(slide-toggle): add ripple focus indicator (#3739)

* feat(slide-toggle): add ripple focus indicator

* Introduces a focus indiactor using the persistent ripples.

* Address comments
This commit is contained in:
Paul Gschwendtner
2017-03-29 23:31:42 +02:00
committed by tinayuangao
parent bedf5a1e42
commit c4ec662390
4 changed files with 82 additions and 40 deletions
+7 -4
View File
@@ -1,16 +1,19 @@
import {NgModule, ModuleWithProviders} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {GestureConfig, CompatibilityModule} from '../core';
import {MdSlideToggle} from './slide-toggle';
import {MdRippleModule} from '../core/ripple/index';
import {
GestureConfig, CompatibilityModule, MdRippleModule, FOCUS_ORIGIN_MONITOR_PROVIDER
} from '../core';
@NgModule({
imports: [FormsModule, MdRippleModule, CompatibilityModule],
exports: [MdSlideToggle, CompatibilityModule],
declarations: [MdSlideToggle],
providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig}],
providers: [
FOCUS_ORIGIN_MONITOR_PROVIDER,
{ provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig }
],
})
export class MdSlideToggleModule {
/** @deprecated */
-2
View File
@@ -11,8 +11,6 @@
[attr.name]="name"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledby"
(blur)="_onInputBlur()"
(focus)="_onInputFocus()"
(change)="_onChangeEvent($event)"
(click)="_onInputClick($event)">
+21 -11
View File
@@ -5,6 +5,7 @@ import {FormsModule, NgControl, ReactiveFormsModule, FormControl} from '@angular
import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './index';
import {TestGestureConfig} from '../slider/test-gesture-config';
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer';
describe('MdSlideToggle', () => {
@@ -268,6 +269,26 @@ describe('MdSlideToggle', () => {
fixture.detectChanges();
});
it('should show a ripple when focused by a keyboard action', fakeAsync(() => {
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripples to be present.');
dispatchFakeEvent(inputElement, 'keydown');
dispatchFakeEvent(inputElement, 'focus');
tick(RIPPLE_FADE_IN_DURATION);
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
.toBe(1, 'Expected the focus ripple to be showing up.');
dispatchFakeEvent(inputElement, 'blur');
tick(RIPPLE_FADE_OUT_DURATION);
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected focus ripple to be removed.');
}));
it('should have the correct control state initially and after interaction', () => {
// The control should start off valid, pristine, and untouched.
expect(slideToggleControl.valid).toBe(true);
@@ -327,15 +348,6 @@ describe('MdSlideToggle', () => {
});
}));
it('should correctly set the slide-toggle to checked on focus', () => {
expect(slideToggleElement.classList).not.toContain('mat-slide-toggle-focused');
dispatchFakeEvent(inputElement, 'focus');
fixture.detectChanges();
expect(slideToggleElement.classList).toContain('mat-slide-toggle-focused');
});
it('should forward the required attribute', () => {
testComponent.isRequired = true;
fixture.detectChanges();
@@ -349,14 +361,12 @@ describe('MdSlideToggle', () => {
});
it('should focus on underlying input element when focus() is called', () => {
expect(slideToggleElement.classList).not.toContain('mat-slide-toggle-focused');
expect(document.activeElement).not.toBe(inputElement);
slideToggle.focus();
fixture.detectChanges();
expect(document.activeElement).toBe(inputElement);
expect(slideToggleElement.classList).toContain('mat-slide-toggle-focused');
});
it('should set a element class if labelPosition is set to before', () => {
+54 -23
View File
@@ -10,11 +10,20 @@ import {
AfterContentInit,
ViewChild,
ViewEncapsulation,
OnDestroy,
} from '@angular/core';
import {
applyCssTransform,
coerceBooleanProperty,
HammerInput,
FocusOriginMonitor,
FocusOrigin,
MdRipple,
RippleRef
} from '../core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {applyCssTransform, coerceBooleanProperty, HammerInput} from '../core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
@@ -41,8 +50,6 @@ let nextId = 0;
'[class.mat-slide-toggle]': 'true',
'[class.mat-checked]': 'checked',
'[class.mat-disabled]': 'disabled',
// This mat-slide-toggle prefix will change, once the temporary ripple is removed.
'[class.mat-slide-toggle-focused]': '_hasFocus',
'[class.mat-slide-toggle-label-before]': 'labelPosition == "before"',
'(mousedown)': '_setMousedown()'
},
@@ -52,7 +59,7 @@ let nextId = 0;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
export class MdSlideToggle implements OnDestroy, AfterContentInit, ControlValueAccessor {
private onChange = (_: any) => {};
private onTouched = () => {};
@@ -67,8 +74,11 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
private _required: boolean = false;
private _disableRipple: boolean = false;
// Needs to be public to support AOT compilation (as host binding).
_hasFocus: boolean = false;
/** Reference to the focus state ripple. */
private _focusRipple: RippleRef;
/** Subscription to focus-origin changes. */
private _focusOriginSubscription: Subscription;
/** Name value will be applied to the input element if present */
@Input() name: string = null;
@@ -110,12 +120,31 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
/** Returns the unique id for the visual hidden input. */
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
/** Reference to the underlying input element. */
@ViewChild('input') _inputElement: ElementRef;
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
/** Reference to the ripple directive on the thumb container. */
@ViewChild(MdRipple) _ripple: MdRipple;
constructor(private _elementRef: ElementRef,
private _renderer: Renderer,
private _focusOriginMonitor: FocusOriginMonitor) {}
ngAfterContentInit() {
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
this._focusOriginSubscription = this._focusOriginMonitor
.monitor(this._inputElement.nativeElement, this._renderer, false)
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
}
ngOnDestroy() {
this._focusOriginMonitor.unmonitor(this._inputElement.nativeElement);
if (this._focusOriginSubscription) {
this._focusOriginSubscription.unsubscribe();
this._focusOriginSubscription = null;
}
}
/**
@@ -162,19 +191,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
setTimeout(() => this._isMousedown = false, 100);
}
_onInputFocus() {
// Only show the focus / ripple indicator when the focus was not triggered by a mouse
// interaction on the component.
if (!this._isMousedown) {
this._hasFocus = true;
}
}
_onInputBlur() {
this._hasFocus = false;
this.onTouched();
}
/** Implemented as part of ControlValueAccessor. */
writeValue(value: any): void {
this.checked = value;
@@ -197,8 +213,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
/** Focuses the slide-toggle. */
focus() {
this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus');
this._onInputFocus();
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program');
}
/** Whether the slide-toggle is checked. */
@@ -223,6 +238,22 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
this.checked = !this.checked;
}
/** Function is called whenever the focus changes for the input element. */
private _onInputFocusChange(focusOrigin: FocusOrigin) {
if (!this._focusRipple && focusOrigin === 'keyboard') {
// For keyboard focus show a persistent ripple as focus indicator.
this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true});
} else if (!focusOrigin) {
this.onTouched();
// Fade out and clear the focus ripple if one is currently present.
if (this._focusRipple) {
this._focusRipple.fadeOut();
this._focusRipple = null;
}
}
}
private _updateColor(newColor: string) {
this._setElementColor(this._color, false);
this._setElementColor(newColor, true);