From 01188d933e03320aa2d128defd1cd786df24f70a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 4 Mar 2017 23:54:10 +0100 Subject: [PATCH] fix(checkbox): add focus indication (#3403) Fixes #3102 --- src/lib/checkbox/checkbox.html | 1 - src/lib/checkbox/checkbox.spec.ts | 39 ++++++++++++++++++- src/lib/checkbox/checkbox.ts | 64 ++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/lib/checkbox/checkbox.html b/src/lib/checkbox/checkbox.html index 209ceb8b8605..8c979eff04ef 100644 --- a/src/lib/checkbox/checkbox.html +++ b/src/lib/checkbox/checkbox.html @@ -12,7 +12,6 @@ [indeterminate]="indeterminate" [attr.aria-label]="ariaLabel" [attr.aria-labelledby]="ariaLabelledby" - (focus)="_onInputFocus()" (blur)="_onInputBlur()" (change)="_onInteractionEvent($event)" (click)="_onInputClick($event)"> diff --git a/src/lib/checkbox/checkbox.spec.ts b/src/lib/checkbox/checkbox.spec.ts index 857c44348208..a6342eaa76e1 100644 --- a/src/lib/checkbox/checkbox.spec.ts +++ b/src/lib/checkbox/checkbox.spec.ts @@ -1,4 +1,11 @@ -import {async, fakeAsync, flushMicrotasks, ComponentFixture, TestBed} from '@angular/core/testing'; +import { + async, + fakeAsync, + flushMicrotasks, + ComponentFixture, + TestBed, + tick, +} from '@angular/core/testing'; import {NgControl, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms'; import {Component, DebugElement} from '@angular/core'; import {By} from '@angular/platform-browser'; @@ -6,10 +13,22 @@ import {MdCheckbox, MdCheckboxChange, MdCheckboxModule} from './checkbox'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {dispatchFakeEvent} from '../core/testing/dispatch-events'; +import {FocusOriginMonitor, FocusOrigin} from '../core'; +import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer'; +import {Subject} from 'rxjs/Subject'; describe('MdCheckbox', () => { let fixture: ComponentFixture; + let fakeFocusOriginMonitorSubject: Subject = new Subject(); + let fakeFocusOriginMonitor = { + monitor: () => fakeFocusOriginMonitorSubject.asObservable(), + unmonitor: () => {}, + focusVia: (element: HTMLElement, renderer: any, focusOrigin: FocusOrigin) => { + element.focus(); + fakeFocusOriginMonitorSubject.next(focusOrigin); + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -27,6 +46,7 @@ describe('MdCheckbox', () => { ], providers: [ {provide: ViewportRuler, useClass: FakeViewportRuler}, + {provide: FocusOriginMonitor, useValue: fakeFocusOriginMonitor} ] }); @@ -321,6 +341,23 @@ describe('MdCheckbox', () => { expect(inputElement.value).toBe('basic_checkbox'); }); + it('should show a ripple when focused by a keyboard action', fakeAsync(() => { + expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) + .toBe(0, 'Expected no ripples on load.'); + + fakeFocusOriginMonitorSubject.next('keyboard'); + tick(RIPPLE_FADE_IN_DURATION); + + expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) + .toBe(1, 'Expected ripple after element is focused.'); + + dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur'); + tick(RIPPLE_FADE_OUT_DURATION); + + expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length) + .toBe(0, 'Expected no ripple after element is blurred.'); + })); + describe('ripple elements', () => { it('should show ripples on label mousedown', () => { diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index 429666335b65..a935e2d2ef14 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -12,11 +12,20 @@ import { NgModule, ModuleWithProviders, ViewChild, + AfterViewInit, + OnDestroy, } from '@angular/core'; import {CommonModule} from '@angular/common'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {MdRippleModule, CompatibilityModule} from '../core'; +import {Subscription} from 'rxjs/Subscription'; +import { + CompatibilityModule, + MdRippleModule, + MdRipple, + RippleRef, + FocusOriginMonitor, +} from '../core'; /** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */ @@ -73,13 +82,12 @@ export class MdCheckboxChange { '[class.mat-checkbox-checked]': 'checked', '[class.mat-checkbox-disabled]': 'disabled', '[class.mat-checkbox-label-before]': 'labelPosition == "before"', - '[class.mat-checkbox-focused]': '_hasFocus', }, providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class MdCheckbox implements ControlValueAccessor { +export class MdCheckbox implements ControlValueAccessor, AfterViewInit, OnDestroy { /** * Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will * take precedence so this may be omitted. @@ -157,6 +165,8 @@ export class MdCheckbox implements ControlValueAccessor { /** The native ` element */ @ViewChild('input') _inputElement: ElementRef; + @ViewChild(MdRipple) _ripple: MdRipple; + /** * Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor. * @docs-private @@ -175,14 +185,38 @@ export class MdCheckbox implements ControlValueAccessor { private _controlValueAccessorChangeFn: (value: any) => void = (value) => {}; - _hasFocus: boolean = false; + /** Reference to the focused state ripple. */ + private _focusedRipple: RippleRef; + + /** Reference to the focus origin monitor subscription. */ + private _focusedSubscription: Subscription; constructor(private _renderer: Renderer, private _elementRef: ElementRef, - private _changeDetectorRef: ChangeDetectorRef) { + private _changeDetectorRef: ChangeDetectorRef, + private _focusOriginMonitor: FocusOriginMonitor) { this.color = 'accent'; } + ngAfterViewInit() { + this._focusedSubscription = this._focusOriginMonitor + .monitor(this._inputElement.nativeElement, this._renderer, false) + .subscribe(focusOrigin => { + if (!this._focusedRipple && focusOrigin === 'keyboard') { + this._focusedRipple = this._ripple.launch(0, 0, { persistent: true, centered: true }); + } + }); + } + + ngOnDestroy() { + this._focusOriginMonitor.unmonitor(this._inputElement.nativeElement); + + if (this._focusedSubscription) { + this._focusedSubscription.unsubscribe(); + this._focusedSubscription = null; + } + } + /** * Whether the checkbox is checked. Note that setting `checked` will immediately set * `indeterminate` to false. @@ -315,14 +349,9 @@ export class MdCheckbox implements ControlValueAccessor { this.change.emit(event); } - /** Informs the component when the input has focus so that we can style accordingly */ - _onInputFocus() { - this._hasFocus = true; - } - /** Informs the component when we lose focus in order to style accordingly */ _onInputBlur() { - this._hasFocus = false; + this._removeFocusedRipple(); this.onTouched(); } @@ -348,6 +377,8 @@ export class MdCheckbox implements ControlValueAccessor { // Preventing bubbling for the second event will solve that issue. event.stopPropagation(); + this._removeFocusedRipple(); + if (!this.disabled) { this.toggle(); this._transitionCheckState( @@ -362,8 +393,7 @@ export class MdCheckbox implements ControlValueAccessor { /** Focuses the checkbox. */ focus(): void { - this._renderer.invokeElementMethod(this._inputElement.nativeElement, 'focus'); - this._onInputFocus(); + this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, this._renderer, 'keyboard'); } _onInteractionEvent(event: Event) { @@ -405,6 +435,13 @@ export class MdCheckbox implements ControlValueAccessor { return `mat-checkbox-anim-${animSuffix}`; } + /** Fades out the focused state ripple. */ + private _removeFocusedRipple(): void { + if (this._focusedRipple) { + this._focusedRipple.fadeOut(); + this._focusedRipple = null; + } + } } @@ -412,6 +449,7 @@ export class MdCheckbox implements ControlValueAccessor { imports: [CommonModule, MdRippleModule, CompatibilityModule], exports: [MdCheckbox, CompatibilityModule], declarations: [MdCheckbox], + providers: [FocusOriginMonitor] }) export class MdCheckboxModule { /** @deprecated */