Skip to content

Commit c4ec662

Browse files
devversiontinayuangao
authored andcommitted
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
1 parent bedf5a1 commit c4ec662

File tree

4 files changed

+82
-40
lines changed

4 files changed

+82
-40
lines changed

src/lib/slide-toggle/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import {NgModule, ModuleWithProviders} from '@angular/core';
22
import {FormsModule} from '@angular/forms';
33
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
4-
import {GestureConfig, CompatibilityModule} from '../core';
54
import {MdSlideToggle} from './slide-toggle';
6-
import {MdRippleModule} from '../core/ripple/index';
7-
5+
import {
6+
GestureConfig, CompatibilityModule, MdRippleModule, FOCUS_ORIGIN_MONITOR_PROVIDER
7+
} from '../core';
88

99
@NgModule({
1010
imports: [FormsModule, MdRippleModule, CompatibilityModule],
1111
exports: [MdSlideToggle, CompatibilityModule],
1212
declarations: [MdSlideToggle],
13-
providers: [{provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig}],
13+
providers: [
14+
FOCUS_ORIGIN_MONITOR_PROVIDER,
15+
{ provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig }
16+
],
1417
})
1518
export class MdSlideToggleModule {
1619
/** @deprecated */

src/lib/slide-toggle/slide-toggle.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
[attr.name]="name"
1212
[attr.aria-label]="ariaLabel"
1313
[attr.aria-labelledby]="ariaLabelledby"
14-
(blur)="_onInputBlur()"
15-
(focus)="_onInputFocus()"
1614
(change)="_onChangeEvent($event)"
1715
(click)="_onInputClick($event)">
1816

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {FormsModule, NgControl, ReactiveFormsModule, FormControl} from '@angular
55
import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './index';
66
import {TestGestureConfig} from '../slider/test-gesture-config';
77
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
8+
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer';
89

910
describe('MdSlideToggle', () => {
1011

@@ -268,6 +269,26 @@ describe('MdSlideToggle', () => {
268269
fixture.detectChanges();
269270
});
270271

272+
it('should show a ripple when focused by a keyboard action', fakeAsync(() => {
273+
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
274+
.toBe(0, 'Expected no ripples to be present.');
275+
276+
dispatchFakeEvent(inputElement, 'keydown');
277+
dispatchFakeEvent(inputElement, 'focus');
278+
279+
tick(RIPPLE_FADE_IN_DURATION);
280+
281+
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
282+
.toBe(1, 'Expected the focus ripple to be showing up.');
283+
284+
dispatchFakeEvent(inputElement, 'blur');
285+
286+
tick(RIPPLE_FADE_OUT_DURATION);
287+
288+
expect(slideToggleElement.querySelectorAll('.mat-ripple-element').length)
289+
.toBe(0, 'Expected focus ripple to be removed.');
290+
}));
291+
271292
it('should have the correct control state initially and after interaction', () => {
272293
// The control should start off valid, pristine, and untouched.
273294
expect(slideToggleControl.valid).toBe(true);
@@ -327,15 +348,6 @@ describe('MdSlideToggle', () => {
327348
});
328349
}));
329350

330-
it('should correctly set the slide-toggle to checked on focus', () => {
331-
expect(slideToggleElement.classList).not.toContain('mat-slide-toggle-focused');
332-
333-
dispatchFakeEvent(inputElement, 'focus');
334-
fixture.detectChanges();
335-
336-
expect(slideToggleElement.classList).toContain('mat-slide-toggle-focused');
337-
});
338-
339351
it('should forward the required attribute', () => {
340352
testComponent.isRequired = true;
341353
fixture.detectChanges();
@@ -349,14 +361,12 @@ describe('MdSlideToggle', () => {
349361
});
350362

351363
it('should focus on underlying input element when focus() is called', () => {
352-
expect(slideToggleElement.classList).not.toContain('mat-slide-toggle-focused');
353364
expect(document.activeElement).not.toBe(inputElement);
354365

355366
slideToggle.focus();
356367
fixture.detectChanges();
357368

358369
expect(document.activeElement).toBe(inputElement);
359-
expect(slideToggleElement.classList).toContain('mat-slide-toggle-focused');
360370
});
361371

362372
it('should set a element class if labelPosition is set to before', () => {

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

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ import {
1010
AfterContentInit,
1111
ViewChild,
1212
ViewEncapsulation,
13+
OnDestroy,
1314
} from '@angular/core';
15+
import {
16+
applyCssTransform,
17+
coerceBooleanProperty,
18+
HammerInput,
19+
FocusOriginMonitor,
20+
FocusOrigin,
21+
MdRipple,
22+
RippleRef
23+
} from '../core';
1424
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
15-
import {applyCssTransform, coerceBooleanProperty, HammerInput} from '../core';
1625
import {Observable} from 'rxjs/Observable';
17-
26+
import {Subscription} from 'rxjs/Subscription';
1827

1928
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
2029
provide: NG_VALUE_ACCESSOR,
@@ -41,8 +50,6 @@ let nextId = 0;
4150
'[class.mat-slide-toggle]': 'true',
4251
'[class.mat-checked]': 'checked',
4352
'[class.mat-disabled]': 'disabled',
44-
// This mat-slide-toggle prefix will change, once the temporary ripple is removed.
45-
'[class.mat-slide-toggle-focused]': '_hasFocus',
4653
'[class.mat-slide-toggle-label-before]': 'labelPosition == "before"',
4754
'(mousedown)': '_setMousedown()'
4855
},
@@ -52,7 +59,7 @@ let nextId = 0;
5259
encapsulation: ViewEncapsulation.None,
5360
changeDetection: ChangeDetectionStrategy.OnPush
5461
})
55-
export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
62+
export class MdSlideToggle implements OnDestroy, AfterContentInit, ControlValueAccessor {
5663

5764
private onChange = (_: any) => {};
5865
private onTouched = () => {};
@@ -67,8 +74,11 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
6774
private _required: boolean = false;
6875
private _disableRipple: boolean = false;
6976

70-
// Needs to be public to support AOT compilation (as host binding).
71-
_hasFocus: boolean = false;
77+
/** Reference to the focus state ripple. */
78+
private _focusRipple: RippleRef;
79+
80+
/** Subscription to focus-origin changes. */
81+
private _focusOriginSubscription: Subscription;
7282

7383
/** Name value will be applied to the input element if present */
7484
@Input() name: string = null;
@@ -110,12 +120,31 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
110120
/** Returns the unique id for the visual hidden input. */
111121
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
112122

123+
/** Reference to the underlying input element. */
113124
@ViewChild('input') _inputElement: ElementRef;
114125

115-
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
126+
/** Reference to the ripple directive on the thumb container. */
127+
@ViewChild(MdRipple) _ripple: MdRipple;
128+
129+
constructor(private _elementRef: ElementRef,
130+
private _renderer: Renderer,
131+
private _focusOriginMonitor: FocusOriginMonitor) {}
116132

117133
ngAfterContentInit() {
118134
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
135+
136+
this._focusOriginSubscription = this._focusOriginMonitor
137+
.monitor(this._inputElement.nativeElement, this._renderer, false)
138+
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
139+
}
140+
141+
ngOnDestroy() {
142+
this._focusOriginMonitor.unmonitor(this._inputElement.nativeElement);
143+
144+
if (this._focusOriginSubscription) {
145+
this._focusOriginSubscription.unsubscribe();
146+
this._focusOriginSubscription = null;
147+
}
119148
}
120149

121150
/**
@@ -162,19 +191,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
162191
setTimeout(() => this._isMousedown = false, 100);
163192
}
164193

165-
_onInputFocus() {
166-
// Only show the focus / ripple indicator when the focus was not triggered by a mouse
167-
// interaction on the component.
168-
if (!this._isMousedown) {
169-
this._hasFocus = true;
170-
}
171-
}
172-
173-
_onInputBlur() {
174-
this._hasFocus = false;
175-
this.onTouched();
176-
}
177-
178194
/** Implemented as part of ControlValueAccessor. */
179195
writeValue(value: any): void {
180196
this.checked = value;
@@ -197,8 +213,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
197213

198214
/** Focuses the slide-toggle. */
199215
focus() {
200-
this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus');
201-
this._onInputFocus();
216+
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'program');
202217
}
203218

204219
/** Whether the slide-toggle is checked. */
@@ -223,6 +238,22 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
223238
this.checked = !this.checked;
224239
}
225240

241+
/** Function is called whenever the focus changes for the input element. */
242+
private _onInputFocusChange(focusOrigin: FocusOrigin) {
243+
if (!this._focusRipple && focusOrigin === 'keyboard') {
244+
// For keyboard focus show a persistent ripple as focus indicator.
245+
this._focusRipple = this._ripple.launch(0, 0, {persistent: true, centered: true});
246+
} else if (!focusOrigin) {
247+
this.onTouched();
248+
249+
// Fade out and clear the focus ripple if one is currently present.
250+
if (this._focusRipple) {
251+
this._focusRipple.fadeOut();
252+
this._focusRipple = null;
253+
}
254+
}
255+
}
256+
226257
private _updateColor(newColor: string) {
227258
this._setElementColor(this._color, false);
228259
this._setElementColor(newColor, true);

0 commit comments

Comments
 (0)