mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
tinayuangao
parent
bedf5a1e42
commit
c4ec662390
@@ -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 */
|
||||
|
||||
@@ -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)">
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user