Skip to content

Commit 01188d9

Browse files
crisbetokara
authored andcommitted
fix(checkbox): add focus indication (#3403)
Fixes #3102
1 parent 9d4d692 commit 01188d9

File tree

3 files changed

+89
-15
lines changed

3 files changed

+89
-15
lines changed

src/lib/checkbox/checkbox.html

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
[indeterminate]="indeterminate"
1313
[attr.aria-label]="ariaLabel"
1414
[attr.aria-labelledby]="ariaLabelledby"
15-
(focus)="_onInputFocus()"
1615
(blur)="_onInputBlur()"
1716
(change)="_onInteractionEvent($event)"
1817
(click)="_onInputClick($event)">

src/lib/checkbox/checkbox.spec.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
import {async, fakeAsync, flushMicrotasks, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {
2+
async,
3+
fakeAsync,
4+
flushMicrotasks,
5+
ComponentFixture,
6+
TestBed,
7+
tick,
8+
} from '@angular/core/testing';
29
import {NgControl, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
310
import {Component, DebugElement} from '@angular/core';
411
import {By} from '@angular/platform-browser';
512
import {MdCheckbox, MdCheckboxChange, MdCheckboxModule} from './checkbox';
613
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
714
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
815
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
16+
import {FocusOriginMonitor, FocusOrigin} from '../core';
17+
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer';
18+
import {Subject} from 'rxjs/Subject';
919

1020

1121
describe('MdCheckbox', () => {
1222
let fixture: ComponentFixture<any>;
23+
let fakeFocusOriginMonitorSubject: Subject<FocusOrigin> = new Subject();
24+
let fakeFocusOriginMonitor = {
25+
monitor: () => fakeFocusOriginMonitorSubject.asObservable(),
26+
unmonitor: () => {},
27+
focusVia: (element: HTMLElement, renderer: any, focusOrigin: FocusOrigin) => {
28+
element.focus();
29+
fakeFocusOriginMonitorSubject.next(focusOrigin);
30+
}
31+
};
1332

1433
beforeEach(async(() => {
1534
TestBed.configureTestingModule({
@@ -27,6 +46,7 @@ describe('MdCheckbox', () => {
2746
],
2847
providers: [
2948
{provide: ViewportRuler, useClass: FakeViewportRuler},
49+
{provide: FocusOriginMonitor, useValue: fakeFocusOriginMonitor}
3050
]
3151
});
3252

@@ -321,6 +341,23 @@ describe('MdCheckbox', () => {
321341
expect(inputElement.value).toBe('basic_checkbox');
322342
});
323343

344+
it('should show a ripple when focused by a keyboard action', fakeAsync(() => {
345+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
346+
.toBe(0, 'Expected no ripples on load.');
347+
348+
fakeFocusOriginMonitorSubject.next('keyboard');
349+
tick(RIPPLE_FADE_IN_DURATION);
350+
351+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
352+
.toBe(1, 'Expected ripple after element is focused.');
353+
354+
dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur');
355+
tick(RIPPLE_FADE_OUT_DURATION);
356+
357+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
358+
.toBe(0, 'Expected no ripple after element is blurred.');
359+
}));
360+
324361
describe('ripple elements', () => {
325362

326363
it('should show ripples on label mousedown', () => {

src/lib/checkbox/checkbox.ts

+51-13
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@ import {
1212
NgModule,
1313
ModuleWithProviders,
1414
ViewChild,
15+
AfterViewInit,
16+
OnDestroy,
1517
} from '@angular/core';
1618
import {CommonModule} from '@angular/common';
1719
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1820
import {coerceBooleanProperty} from '../core/coercion/boolean-property';
19-
import {MdRippleModule, CompatibilityModule} from '../core';
21+
import {Subscription} from 'rxjs/Subscription';
22+
import {
23+
CompatibilityModule,
24+
MdRippleModule,
25+
MdRipple,
26+
RippleRef,
27+
FocusOriginMonitor,
28+
} from '../core';
2029

2130

2231
/** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */
@@ -73,13 +82,12 @@ export class MdCheckboxChange {
7382
'[class.mat-checkbox-checked]': 'checked',
7483
'[class.mat-checkbox-disabled]': 'disabled',
7584
'[class.mat-checkbox-label-before]': 'labelPosition == "before"',
76-
'[class.mat-checkbox-focused]': '_hasFocus',
7785
},
7886
providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR],
7987
encapsulation: ViewEncapsulation.None,
8088
changeDetection: ChangeDetectionStrategy.OnPush
8189
})
82-
export class MdCheckbox implements ControlValueAccessor {
90+
export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestroy {
8391
/**
8492
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
8593
* take precedence so this may be omitted.
@@ -157,6 +165,8 @@ export class MdCheckbox implements ControlValueAccessor {
157165
/** The native `<input type="checkbox"> element */
158166
@ViewChild('input') _inputElement: ElementRef;
159167

168+
@ViewChild(MdRipple) _ripple: MdRipple;
169+
160170
/**
161171
* Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor.
162172
* @docs-private
@@ -175,14 +185,38 @@ export class MdCheckbox implements ControlValueAccessor {
175185

176186
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
177187

178-
_hasFocus: boolean = false;
188+
/** Reference to the focused state ripple. */
189+
private _focusedRipple: RippleRef;
190+
191+
/** Reference to the focus origin monitor subscription. */
192+
private _focusedSubscription: Subscription;
179193

180194
constructor(private _renderer: Renderer,
181195
private _elementRef: ElementRef,
182-
private _changeDetectorRef: ChangeDetectorRef) {
196+
private _changeDetectorRef: ChangeDetectorRef,
197+
private _focusOriginMonitor: FocusOriginMonitor) {
183198
this.color = 'accent';
184199
}
185200

201+
ngAfterViewInit() {
202+
this._focusedSubscription = this._focusOriginMonitor
203+
.monitor(this._inputElement.nativeElement, this._renderer, false)
204+
.subscribe(focusOrigin => {
205+
if (!this._focusedRipple && focusOrigin === 'keyboard') {
206+
this._focusedRipple = this._ripple.launch(0, 0, { persistent: true, centered: true });
207+
}
208+
});
209+
}
210+
211+
ngOnDestroy() {
212+
this._focusOriginMonitor.unmonitor(this._inputElement.nativeElement);
213+
214+
if (this._focusedSubscription) {
215+
this._focusedSubscription.unsubscribe();
216+
this._focusedSubscription = null;
217+
}
218+
}
219+
186220
/**
187221
* Whether the checkbox is checked. Note that setting `checked` will immediately set
188222
* `indeterminate` to false.
@@ -315,14 +349,9 @@ export class MdCheckbox implements ControlValueAccessor {
315349
this.change.emit(event);
316350
}
317351

318-
/** Informs the component when the input has focus so that we can style accordingly */
319-
_onInputFocus() {
320-
this._hasFocus = true;
321-
}
322-
323352
/** Informs the component when we lose focus in order to style accordingly */
324353
_onInputBlur() {
325-
this._hasFocus = false;
354+
this._removeFocusedRipple();
326355
this.onTouched();
327356
}
328357

@@ -348,6 +377,8 @@ export class MdCheckbox implements ControlValueAccessor {
348377
// Preventing bubbling for the second event will solve that issue.
349378
event.stopPropagation();
350379

380+
this._removeFocusedRipple();
381+
351382
if (!this.disabled) {
352383
this.toggle();
353384
this._transitionCheckState(
@@ -362,8 +393,7 @@ export class MdCheckbox implements ControlValueAccessor {
362393

363394
/** Focuses the checkbox. */
364395
focus(): void {
365-
this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus');
366-
this._onInputFocus();
396+
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard');
367397
}
368398

369399
_onInteractionEvent(event: Event) {
@@ -405,13 +435,21 @@ export class MdCheckbox implements ControlValueAccessor {
405435
return `mat-checkbox-anim-${animSuffix}`;
406436
}
407437

438+
/** Fades out the focused state ripple. */
439+
private _removeFocusedRipple(): void {
440+
if (this._focusedRipple) {
441+
this._focusedRipple.fadeOut();
442+
this._focusedRipple = null;
443+
}
444+
}
408445
}
409446

410447

411448
@NgModule({
412449
imports: [CommonModule, MdRippleModule, CompatibilityModule],
413450
exports: [MdCheckbox, CompatibilityModule],
414451
declarations: [MdCheckbox],
452+
providers: [FocusOriginMonitor]
415453
})
416454
export class MdCheckboxModule {
417455
/** @deprecated */

0 commit comments

Comments
 (0)