fix(datepicker): restore focus to trigger element (#4804)

This commit is contained in:
Kristiyan Kostadinov
2017-06-16 20:58:23 +02:00
committed by Kara
parent 223b237741
commit 8860090cea
2 changed files with 73 additions and 18 deletions
+54 -7
View File
@@ -1,14 +1,19 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MdDatepickerModule} from './index';
import {Component, ViewChild} from '@angular/core';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
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 {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
import {MdDatepickerModule} from './index';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
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;
}
+19 -11
View File
@@ -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,6 +154,9 @@ 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,
@@ -165,11 +164,12 @@ export class MdDatepicker<D> implements OnDestroy {
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;
}