Skip to content

Commit 19ce3f8

Browse files
committed
fix(checkbox): no focus indication
Fixes the checkbox not having focus indication when the element is focused via the keyboard. Fixes #3102.
1 parent 4e4c6a6 commit 19ce3f8

File tree

3 files changed

+89
-15
lines changed

3 files changed

+89
-15
lines changed

src/lib/checkbox/checkbox.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
[indeterminate]="indeterminate"
1212
[attr.aria-label]="ariaLabel"
1313
[attr.aria-labelledby]="ariaLabelledby"
14-
(focus)="_onInputFocus()"
1514
(blur)="_onInputBlur()"
1615
(change)="_onInteractionEvent($event)"
1716
(click)="_onInputClick($event)">

src/lib/checkbox/checkbox.spec.ts

Lines changed: 38 additions & 1 deletion
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

@@ -314,6 +334,23 @@ describe('MdCheckbox', () => {
314334
expect(document.activeElement).toBe(inputElement);
315335
});
316336

337+
it('should show a ripple when focused by a keyboard action', fakeAsync(() => {
338+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
339+
.toBe(0, 'Expected no ripples on load.');
340+
341+
fakeFocusOriginMonitorSubject.next('keyboard');
342+
tick(RIPPLE_FADE_IN_DURATION);
343+
344+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
345+
.toBe(1, 'Expected ripple after element is focused.');
346+
347+
dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur');
348+
tick(RIPPLE_FADE_OUT_DURATION);
349+
350+
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
351+
.toBe(0, 'Expected no ripple after element is blurred.');
352+
}));
353+
317354
describe('ripple elements', () => {
318355

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

src/lib/checkbox/checkbox.ts

Lines changed: 51 additions & 13 deletions
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.
@@ -154,6 +162,8 @@ export class MdCheckbox implements ControlValueAccessor {
154162
/** The native `<input type="checkbox"> element */
155163
@ViewChild('input') _inputElement: ElementRef;
156164

165+
@ViewChild(MdRipple) _ripple: MdRipple;
166+
157167
/**
158168
* Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor.
159169
* @docs-private
@@ -172,14 +182,38 @@ export class MdCheckbox implements ControlValueAccessor {
172182

173183
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
174184

175-
_hasFocus: boolean = false;
185+
/** Reference to the focused state ripple. */
186+
private _focusedRipple: RippleRef;
187+
188+
/** Reference to the focus origin monitor subscription. */
189+
private _focusedSubscription: Subscription;
176190

177191
constructor(private _renderer: Renderer,
178192
private _elementRef: ElementRef,
179-
private _changeDetectorRef: ChangeDetectorRef) {
193+
private _changeDetectorRef: ChangeDetectorRef,
194+
private _focusOriginMonitor: FocusOriginMonitor) {
180195
this.color = 'accent';
181196
}
182197

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

316-
/** Informs the component when the input has focus so that we can style accordingly */
317-
_onInputFocus() {
318-
this._hasFocus = true;
319-
}
320-
321350
/** Informs the component when we lose focus in order to style accordingly */
322351
_onInputBlur() {
323-
this._hasFocus = false;
352+
this._removeFocusedRipple();
324353
this.onTouched();
325354
}
326355

@@ -346,6 +375,8 @@ export class MdCheckbox implements ControlValueAccessor {
346375
// Preventing bubbling for the second event will solve that issue.
347376
event.stopPropagation();
348377

378+
this._removeFocusedRipple();
379+
349380
if (!this.disabled) {
350381
this.toggle();
351382

@@ -358,8 +389,7 @@ export class MdCheckbox implements ControlValueAccessor {
358389

359390
/** Focuses the checkbox. */
360391
focus(): void {
361-
this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus');
362-
this._onInputFocus();
392+
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard');
363393
}
364394

365395
_onInteractionEvent(event: Event) {
@@ -399,13 +429,21 @@ export class MdCheckbox implements ControlValueAccessor {
399429
return `mat-checkbox-anim-${animSuffix}`;
400430
}
401431

432+
/** Fades out the focused state ripple. */
433+
private _removeFocusedRipple(): void {
434+
if (this._focusedRipple) {
435+
this._focusedRipple.fadeOut();
436+
this._focusedRipple = null;
437+
}
438+
}
402439
}
403440

404441

405442
@NgModule({
406443
imports: [CommonModule, MdRippleModule, CompatibilityModule],
407444
exports: [MdCheckbox, CompatibilityModule],
408445
declarations: [MdCheckbox],
446+
providers: [FocusOriginMonitor]
409447
})
410448
export class MdCheckboxModule {
411449
/** @deprecated */

0 commit comments

Comments
 (0)