Skip to content

Commit

Permalink
fix(datepicker): restore focus to trigger element (#4804)
Browse files Browse the repository at this point in the history
crisbeto authored and kara committed Jun 16, 2017
1 parent 223b237 commit 8860090
Showing 2 changed files with 72 additions and 17 deletions.
59 changes: 53 additions & 6 deletions src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `
<input [mdDatepicker]="d">
<button [mdDatepickerToggle]="d"></button>
<md-datepicker #d [touchUi]="true"></md-datepicker>
<md-datepicker #d [touchUi]="touchUI"></md-datepicker>
`,
})
class DatepickerWithToggle {
@ViewChild('d') datepicker: MdDatepicker<Date>;
touchUI = true;
}


30 changes: 19 additions & 11 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -77,16 +79,10 @@ export class MdDatepickerContent<D> 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,18 +154,22 @@ export class MdDatepicker<D> implements OnDestroy {
/** The input element this datepicker is associated with. */
private _datepickerInput: MdDatepickerInput<D>;

/** The element that was focused before the datepicker was opened. */
private _focusedElementBeforeOpen: HTMLElement;

private _inputSubscription: Subscription;

constructor(private _dialog: MdDialog,
private _overlay: Overlay,
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef,
@Optional() private _dateAdapter: DateAdapter<D>,
@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<D> 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<D> 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;
}

0 comments on commit 8860090

Please sign in to comment.