From 857d87053a165d774f15f0c514fbe563830a16db Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 25 May 2017 18:38:46 +0200 Subject: [PATCH 1/2] fix(datepicker): restore focus to trigger element * The datepicker now restores focus to whatever element was focused before it was open. * Adds a test for the functionality that closes the datepicker when pressing escape. --- src/lib/datepicker/datepicker.spec.ts | 59 ++++++++++++++++++++++++--- src/lib/datepicker/datepicker.ts | 32 +++++++++------ 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index dd2b266b5282..308d7a3f0da0 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -1,14 +1,19 @@ +import {Component, ViewChild} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {By} from '@angular/platform-browser'; import {MdDatepickerModule} from './index'; -import {Component, ViewChild} from '@angular/core'; import {MdDatepicker} from './datepicker'; import {MdDatepickerInput} from './datepicker-input'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {By} from '@angular/platform-browser'; -import {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events'; import {MdInputModule} from '../input/index'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MdNativeDateModule, DateAdapter, NativeDateAdapter} from '../core/datetime/index'; +import {ESCAPE} from '../core'; +import { + dispatchFakeEvent, + dispatchMouseEvent, + dispatchKeyboardEvent, +} from '../core/testing/dispatch-events'; // When constructing a Date, the month is zero-based. This can be confusing, since people are @@ -107,6 +112,23 @@ describe('MdDatepicker', () => { }); })); + it('should close the popup when pressing ESCAPE', () => { + testComponent.datepicker.open(); + fixture.detectChanges(); + + let content = document.querySelector('.cdk-overlay-pane md-datepicker-content'); + expect(content).toBeTruthy('Expected datepicker to be open.'); + + let keyboadEvent = dispatchKeyboardEvent(content, 'keydown', ESCAPE); + fixture.detectChanges(); + + content = document.querySelector('.cdk-overlay-pane md-datepicker-content'); + + expect(content).toBeFalsy('Expected datepicker to be closed.'); + expect(keyboadEvent.defaultPrevented) + .toBe(true, 'Expected default ESCAPE action to be prevented.'); + }); + it('close should close dialog', async(() => { testComponent.touch = true; fixture.detectChanges(); @@ -425,6 +447,30 @@ describe('MdDatepicker', () => { let toggle = fixture.debugElement.query(By.css('button')).nativeElement; expect(toggle.getAttribute('type')).toBe('button'); }); + + it('should restore focus to the toggle after the calendar is closed', () => { + let toggle = fixture.debugElement.query(By.css('button')).nativeElement; + + fixture.componentInstance.touchUI = false; + fixture.detectChanges(); + + toggle.focus(); + expect(document.activeElement).toBe(toggle, 'Expected toggle to be focused.'); + + fixture.componentInstance.datepicker.open(); + fixture.detectChanges(); + + let pane = document.querySelector('.cdk-overlay-pane'); + + expect(pane).toBeTruthy('Expected calendar to be open.'); + expect(pane.contains(document.activeElement)) + .toBe(true, 'Expected focus to be inside the calendar.'); + + fixture.componentInstance.datepicker.close(); + fixture.detectChanges(); + + expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.'); + }); }); describe('datepicker inside input-container', () => { @@ -767,11 +813,12 @@ class DatepickerWithFormControl { template: ` - + `, }) class DatepickerWithToggle { @ViewChild('d') datepicker: MdDatepicker; + touchUI = true; } diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 76f35a73a372..d8565a2039ae 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -20,7 +20,9 @@ import { ViewContainerRef, ViewEncapsulation, NgZone, + Inject, } from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; import {Overlay} from '../core/overlay/overlay'; import {OverlayRef} from '../core/overlay/overlay-ref'; import {ComponentPortal} from '../core/portal/portal'; @@ -34,7 +36,7 @@ import {Subscription} from 'rxjs/Subscription'; import {MdDialogConfig} from '../dialog/dialog-config'; import {DateAdapter} from '../core/datetime/index'; import {createMissingDateImplError} from './datepicker-errors'; -import {ESCAPE} from '../core/keyboard/keycodes'; +import {ESCAPE, TAB} from '../core/keyboard/keycodes'; import {MdCalendar} from './calendar'; import 'rxjs/add/operator/first'; @@ -77,16 +79,10 @@ export class MdDatepickerContent implements AfterContentInit { * @param event The event. */ _handleKeydown(event: KeyboardEvent): void { - switch (event.keyCode) { - case ESCAPE: - this.datepicker.close(); - break; - default: - // Return so that we don't preventDefault on keys that are not explicitly handled. - return; + if (event.keyCode === ESCAPE) { + this.datepicker.close(); + event.preventDefault(); } - - event.preventDefault(); } } @@ -158,6 +154,9 @@ export class MdDatepicker implements OnDestroy { /** The input element this datepicker is associated with. */ private _datepickerInput: MdDatepickerInput; + /** The element that was focused before the datepicker was opened. */ + private _focusedElementBeforeOpen: HTMLElement; + private _inputSubscription: Subscription; constructor(private _dialog: MdDialog, @@ -165,11 +164,12 @@ export class MdDatepicker implements OnDestroy { private _ngZone: NgZone, private _viewContainerRef: ViewContainerRef, @Optional() private _dateAdapter: DateAdapter, - @Optional() private _dir: Dir) { + @Optional() private _dir: Dir, + @Optional() @Inject(DOCUMENT) private _document: any) { + if (!this._dateAdapter) { throw createMissingDateImplError('DateAdapter'); } - } ngOnDestroy() { @@ -213,6 +213,9 @@ export class MdDatepicker implements OnDestroy { if (!this._datepickerInput) { throw Error('Attempted to open an MdDatepicker with no associated input.'); } + if (this._document) { + this._focusedElementBeforeOpen = this._document.activeElement; + } this.touchUi ? this._openAsDialog() : this._openAsPopup(); this.opened = true; @@ -233,6 +236,11 @@ export class MdDatepicker implements OnDestroy { if (this._calendarPortal && this._calendarPortal.isAttached) { this._calendarPortal.detach(); } + if (this._focusedElementBeforeOpen && 'focus' in this._focusedElementBeforeOpen) { + this._focusedElementBeforeOpen.focus(); + this._focusedElementBeforeOpen = null; + } + this.opened = false; } From 2cd63d7c459c7bee2b10e08e7e0ddad709247419 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 25 May 2017 22:29:54 +0200 Subject: [PATCH 2/2] chore: unused import --- src/lib/datepicker/datepicker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index d8565a2039ae..ce0787ab5ade 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -36,7 +36,7 @@ import {Subscription} from 'rxjs/Subscription'; import {MdDialogConfig} from '../dialog/dialog-config'; import {DateAdapter} from '../core/datetime/index'; import {createMissingDateImplError} from './datepicker-errors'; -import {ESCAPE, TAB} from '../core/keyboard/keycodes'; +import {ESCAPE} from '../core/keyboard/keycodes'; import {MdCalendar} from './calendar'; import 'rxjs/add/operator/first';