Skip to content

fix(datepicker): don't autofocus calendar cell if used outside of overlay #11049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/lib/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import {
ENTER,
RIGHT_ARROW,
} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing';
import {Component} from '@angular/core';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
MockNgZone,
} from '@angular/cdk/testing';
import {Component, NgZone} from '@angular/core';
import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';
import {DEC, FEB, JAN, MatNativeDateModule, NOV} from '@angular/material/core';
import {By} from '@angular/platform-browser';
Expand All @@ -14,6 +19,7 @@ import {MatDatepickerModule} from './datepicker-module';

describe('MatCalendar', () => {
let dir: {value: Direction};
let zone: MockNgZone;

beforeEach(async(() => {
TestBed.configureTestingModule({
Expand All @@ -29,6 +35,7 @@ describe('MatCalendar', () => {
],
providers: [
MatDatepickerIntl,
{provide: NgZone, useFactory: () => zone = new MockNgZone()},
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
],
});
Expand Down Expand Up @@ -150,6 +157,34 @@ describe('MatCalendar', () => {
expect(calendarBodyEl.getAttribute('tabindex')).toBe('-1');
});

it('should not move focus to the active cell on init', () => {
const activeCell =
calendarBodyEl.querySelector('.mat-calendar-body-active')! as HTMLElement;

spyOn(activeCell, 'focus').and.callThrough();
fixture.detectChanges();
zone.simulateZoneExit();

expect(activeCell.focus).not.toHaveBeenCalled();
});

it('should move focus to the active cell when the view changes', () => {
const activeCell =
calendarBodyEl.querySelector('.mat-calendar-body-active')! as HTMLElement;

spyOn(activeCell, 'focus').and.callThrough();
fixture.detectChanges();
zone.simulateZoneExit();

expect(activeCell.focus).not.toHaveBeenCalled();

calendarInstance.currentView = 'multi-year';
fixture.detectChanges();
zone.simulateZoneExit();

expect(activeCell.focus).toHaveBeenCalled();
});

describe('year view', () => {
beforeEach(() => {
dispatchMouseEvent(periodButton, 'click');
Expand Down
48 changes: 42 additions & 6 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';
import {
AfterContentInit,
AfterViewChecked,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Expand All @@ -32,6 +33,12 @@ import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';

/**
* Possible views for the calendar.
* @docs-private
*/
export type MatCalendarView = 'month' | 'year' | 'multi-year';

/** Default header for MatCalendar */
@Component({
moduleId: module.id,
Expand Down Expand Up @@ -162,7 +169,7 @@ export class MatCalendarHeader<D> {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges {
/** An input indicating the type of the header component, if set. */
@Input() headerComponent: ComponentType<any>;

Expand All @@ -171,6 +178,13 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {

private _intlChanges: Subscription;

/**
* Used for scheduling that focus should be moved to the active cell on the next tick.
* We need to schedule it, rather than do it immediately, because we have to wait
* for Angular to re-evaluate the view children.
*/
private _moveFocusOnNextTick = false;

/** A date representing the period (month or year) to start the calendar in. */
@Input()
get startAt(): D | null { return this._startAt; }
Expand All @@ -180,7 +194,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
private _startAt: D | null;

/** Whether the calendar should be started in month or year view. */
@Input() startView: 'month' | 'year' | 'multi-year' = 'month';
@Input() startView: MatCalendarView = 'month';

/** The currently selected date. */
@Input()
Expand Down Expand Up @@ -248,7 +262,12 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
private _clampedActiveDate: D;

/** Whether the calendar is in month view. */
currentView: 'month' | 'year' | 'multi-year';
get currentView(): MatCalendarView { return this._currentView; }
set currentView(value: MatCalendarView) {
this._currentView = value;
this._moveFocusOnNextTick = true;
}
private _currentView: MatCalendarView;

/**
* Emits whenever there is a state change that the header may need to respond to.
Expand Down Expand Up @@ -276,9 +295,17 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {

ngAfterContentInit() {
this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);

this.activeDate = this.startAt || this._dateAdapter.today();
this.currentView = this.startView;

// Assign to the private property since we don't want to move focus on init.
this._currentView = this.startView;
}

ngAfterViewChecked() {
if (this._moveFocusOnNextTick) {
this._moveFocusOnNextTick = false;
this.focusActiveCell();
}
}

ngOnDestroy() {
Expand All @@ -290,7 +317,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
const change = changes.minDate || changes.maxDate || changes.dateFilter;

if (change && !change.firstChange) {
const view = this.monthView || this.yearView || this.multiYearView;
const view = this._getCurrentViewComponent();

if (view) {
view._init();
Expand All @@ -300,6 +327,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
this.stateChanges.next();
}

focusActiveCell() {
this._getCurrentViewComponent()._focusActiveCell();
}

/** Handles date selection in the month view. */
_dateSelected(date: D): void {
if (!this._dateAdapter.sameDate(date, this.selected)) {
Expand Down Expand Up @@ -334,4 +365,9 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}

/** Returns the component instance that corresponds to the current calendar view. */
private _getCurrentViewComponent() {
return this.monthView || this.yearView || this.multiYearView;
}
}
17 changes: 4 additions & 13 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {take, filter} from 'rxjs/operators';
import {
AfterContentInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Expand Down Expand Up @@ -97,7 +97,7 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
inputs: ['color'],
})
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
implements AfterContentInit, CanColor, OnInit, OnDestroy {
implements AfterViewInit, CanColor, OnInit, OnDestroy {

/** Subscription to changes in the overlay's position. */
private _positionChange: Subscription|null;
Expand Down Expand Up @@ -138,17 +138,8 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
});
}

ngAfterContentInit() {
this._focusActiveCell();
}

/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this._elementRef.nativeElement.querySelector('.mat-calendar-body-active').focus();
});
});
ngAfterViewInit() {
this._calendar.focusActiveCell();
}

ngOnDestroy() {
Expand Down
5 changes: 2 additions & 3 deletions src/lib/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class MatMonthView<D> implements AfterContentInit {
@Output() readonly activeDateChange: EventEmitter<D> = new EventEmitter<D>();

/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;

/** The label for this month (e.g. "January 2017"). */
_monthLabel: string;
Expand Down Expand Up @@ -155,7 +155,6 @@ export class MatMonthView<D> implements AfterContentInit {

ngAfterContentInit() {
this._init();
this._focusActiveCell();
}

/** Handles when a new date is selected. */
Expand Down Expand Up @@ -253,7 +252,7 @@ export class MatMonthView<D> implements AfterContentInit {
}

/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
_focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}

Expand Down
5 changes: 2 additions & 3 deletions src/lib/datepicker/multi-year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class MatMultiYearView<D> implements AfterContentInit {
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();

/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;

/** Grid of calendar cells representing the currently displayed years. */
_years: MatCalendarCell[][];
Expand All @@ -125,7 +125,6 @@ export class MatMultiYearView<D> implements AfterContentInit {

ngAfterContentInit() {
this._init();
this._focusActiveCell();
}

/** Initializes this multi-year view. */
Expand Down Expand Up @@ -211,7 +210,7 @@ export class MatMultiYearView<D> implements AfterContentInit {
}

/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
_focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}

Expand Down
5 changes: 2 additions & 3 deletions src/lib/datepicker/year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class MatYearView<D> implements AfterContentInit {
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();

/** The body of calendar table */
@ViewChild(MatCalendarBody) _matCalendarBody;
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;

/** Grid of calendar cells representing the months of the year. */
_months: MatCalendarCell[][];
Expand Down Expand Up @@ -130,7 +130,6 @@ export class MatYearView<D> implements AfterContentInit {

ngAfterContentInit() {
this._init();
this._focusActiveCell();
}

/** Handles when a new month is selected. */
Expand Down Expand Up @@ -211,7 +210,7 @@ export class MatYearView<D> implements AfterContentInit {
}

/** Focuses the active cell after the microtask queue is empty. */
private _focusActiveCell() {
_focusActiveCell() {
this._matCalendarBody._focusActiveCell();
}

Expand Down