Skip to content

Commit 2897797

Browse files
crisbetojosephperrott
authored andcommitted
fix(datepicker): don't autofocus calendar cell if used outside of overlay (#11049)
1 parent 860ce13 commit 2897797

File tree

6 files changed

+89
-30
lines changed

6 files changed

+89
-30
lines changed

src/lib/datepicker/calendar.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import {
22
ENTER,
33
RIGHT_ARROW,
44
} from '@angular/cdk/keycodes';
5-
import {dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing';
6-
import {Component} from '@angular/core';
5+
import {
6+
dispatchFakeEvent,
7+
dispatchKeyboardEvent,
8+
dispatchMouseEvent,
9+
MockNgZone,
10+
} from '@angular/cdk/testing';
11+
import {Component, NgZone} from '@angular/core';
712
import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';
813
import {DEC, FEB, JAN, MatNativeDateModule, NOV} from '@angular/material/core';
914
import {By} from '@angular/platform-browser';
@@ -14,6 +19,7 @@ import {MatDatepickerModule} from './datepicker-module';
1419

1520
describe('MatCalendar', () => {
1621
let dir: {value: Direction};
22+
let zone: MockNgZone;
1723

1824
beforeEach(async(() => {
1925
TestBed.configureTestingModule({
@@ -29,6 +35,7 @@ describe('MatCalendar', () => {
2935
],
3036
providers: [
3137
MatDatepickerIntl,
38+
{provide: NgZone, useFactory: () => zone = new MockNgZone()},
3239
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
3340
],
3441
});
@@ -150,6 +157,34 @@ describe('MatCalendar', () => {
150157
expect(calendarBodyEl.getAttribute('tabindex')).toBe('-1');
151158
});
152159

160+
it('should not move focus to the active cell on init', () => {
161+
const activeCell =
162+
calendarBodyEl.querySelector('.mat-calendar-body-active')! as HTMLElement;
163+
164+
spyOn(activeCell, 'focus').and.callThrough();
165+
fixture.detectChanges();
166+
zone.simulateZoneExit();
167+
168+
expect(activeCell.focus).not.toHaveBeenCalled();
169+
});
170+
171+
it('should move focus to the active cell when the view changes', () => {
172+
const activeCell =
173+
calendarBodyEl.querySelector('.mat-calendar-body-active')! as HTMLElement;
174+
175+
spyOn(activeCell, 'focus').and.callThrough();
176+
fixture.detectChanges();
177+
zone.simulateZoneExit();
178+
179+
expect(activeCell.focus).not.toHaveBeenCalled();
180+
181+
calendarInstance.currentView = 'multi-year';
182+
fixture.detectChanges();
183+
zone.simulateZoneExit();
184+
185+
expect(activeCell.focus).toHaveBeenCalled();
186+
});
187+
153188
describe('year view', () => {
154189
beforeEach(() => {
155190
dispatchMouseEvent(periodButton, 'click');

src/lib/datepicker/calendar.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';
1010
import {
1111
AfterContentInit,
12+
AfterViewChecked,
1213
ChangeDetectionStrategy,
1314
ChangeDetectorRef,
1415
Component,
@@ -32,6 +33,12 @@ import {MatMonthView} from './month-view';
3233
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
3334
import {MatYearView} from './year-view';
3435

36+
/**
37+
* Possible views for the calendar.
38+
* @docs-private
39+
*/
40+
export type MatCalendarView = 'month' | 'year' | 'multi-year';
41+
3542
/** Default header for MatCalendar */
3643
@Component({
3744
moduleId: module.id,
@@ -162,7 +169,7 @@ export class MatCalendarHeader<D> {
162169
encapsulation: ViewEncapsulation.None,
163170
changeDetection: ChangeDetectionStrategy.OnPush,
164171
})
165-
export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
172+
export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges {
166173
/** An input indicating the type of the header component, if set. */
167174
@Input() headerComponent: ComponentType<any>;
168175

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

172179
private _intlChanges: Subscription;
173180

181+
/**
182+
* Used for scheduling that focus should be moved to the active cell on the next tick.
183+
* We need to schedule it, rather than do it immediately, because we have to wait
184+
* for Angular to re-evaluate the view children.
185+
*/
186+
private _moveFocusOnNextTick = false;
187+
174188
/** A date representing the period (month or year) to start the calendar in. */
175189
@Input()
176190
get startAt(): D | null { return this._startAt; }
@@ -180,7 +194,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
180194
private _startAt: D | null;
181195

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

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

250264
/** Whether the calendar is in month view. */
251-
currentView: 'month' | 'year' | 'multi-year';
265+
get currentView(): MatCalendarView { return this._currentView; }
266+
set currentView(value: MatCalendarView) {
267+
this._currentView = value;
268+
this._moveFocusOnNextTick = true;
269+
}
270+
private _currentView: MatCalendarView;
252271

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

277296
ngAfterContentInit() {
278297
this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);
279-
280298
this.activeDate = this.startAt || this._dateAdapter.today();
281-
this.currentView = this.startView;
299+
300+
// Assign to the private property since we don't want to move focus on init.
301+
this._currentView = this.startView;
302+
}
303+
304+
ngAfterViewChecked() {
305+
if (this._moveFocusOnNextTick) {
306+
this._moveFocusOnNextTick = false;
307+
this.focusActiveCell();
308+
}
282309
}
283310

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

292319
if (change && !change.firstChange) {
293-
const view = this.monthView || this.yearView || this.multiYearView;
320+
const view = this._getCurrentViewComponent();
294321

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

330+
focusActiveCell() {
331+
this._getCurrentViewComponent()._focusActiveCell();
332+
}
333+
303334
/** Handles date selection in the month view. */
304335
_dateSelected(date: D): void {
305336
if (!this._dateAdapter.sameDate(date, this.selected)) {
@@ -334,4 +365,9 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
334365
private _getValidDateOrNull(obj: any): D | null {
335366
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
336367
}
368+
369+
/** Returns the component instance that corresponds to the current calendar view. */
370+
private _getCurrentViewComponent() {
371+
return this.monthView || this.yearView || this.multiYearView;
372+
}
337373
}

src/lib/datepicker/datepicker.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
2121
import {DOCUMENT} from '@angular/common';
2222
import {take, filter} from 'rxjs/operators';
2323
import {
24-
AfterContentInit,
24+
AfterViewInit,
2525
ChangeDetectionStrategy,
2626
ChangeDetectorRef,
2727
Component,
@@ -100,7 +100,7 @@ export const _MatDatepickerContentMixinBase = mixinColor(MatDatepickerContentBas
100100
inputs: ['color'],
101101
})
102102
export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
103-
implements AfterContentInit, CanColor, OnInit, OnDestroy {
103+
implements AfterViewInit, CanColor, OnInit, OnDestroy {
104104

105105
/** Subscription to changes in the overlay's position. */
106106
private _positionChange: Subscription|null;
@@ -141,17 +141,8 @@ export class MatDatepickerContent<D> extends _MatDatepickerContentMixinBase
141141
});
142142
}
143143

144-
ngAfterContentInit() {
145-
this._focusActiveCell();
146-
}
147-
148-
/** Focuses the active cell after the microtask queue is empty. */
149-
private _focusActiveCell() {
150-
this._ngZone.runOutsideAngular(() => {
151-
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
152-
this._elementRef.nativeElement.querySelector('.mat-calendar-body-active').focus();
153-
});
154-
});
144+
ngAfterViewInit() {
145+
this._calendar.focusActiveCell();
155146
}
156147

157148
ngOnDestroy() {

src/lib/datepicker/month-view.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class MatMonthView<D> implements AfterContentInit {
106106
@Output() readonly activeDateChange: EventEmitter<D> = new EventEmitter<D>();
107107

108108
/** The body of calendar table */
109-
@ViewChild(MatCalendarBody) _matCalendarBody;
109+
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
110110

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

156156
ngAfterContentInit() {
157157
this._init();
158-
this._focusActiveCell();
159158
}
160159

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

255254
/** Focuses the active cell after the microtask queue is empty. */
256-
private _focusActiveCell() {
255+
_focusActiveCell() {
257256
this._matCalendarBody._focusActiveCell();
258257
}
259258

src/lib/datepicker/multi-year-view.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class MatMultiYearView<D> implements AfterContentInit {
102102
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
103103

104104
/** The body of calendar table */
105-
@ViewChild(MatCalendarBody) _matCalendarBody;
105+
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
106106

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

126126
ngAfterContentInit() {
127127
this._init();
128-
this._focusActiveCell();
129128
}
130129

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

213212
/** Focuses the active cell after the microtask queue is empty. */
214-
private _focusActiveCell() {
213+
_focusActiveCell() {
215214
this._matCalendarBody._focusActiveCell();
216215
}
217216

src/lib/datepicker/year-view.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export class MatYearView<D> implements AfterContentInit {
9797
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
9898

9999
/** The body of calendar table */
100-
@ViewChild(MatCalendarBody) _matCalendarBody;
100+
@ViewChild(MatCalendarBody) _matCalendarBody: MatCalendarBody;
101101

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

131131
ngAfterContentInit() {
132132
this._init();
133-
this._focusActiveCell();
134133
}
135134

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

213212
/** Focuses the active cell after the microtask queue is empty. */
214-
private _focusActiveCell() {
213+
_focusActiveCell() {
215214
this._matCalendarBody._focusActiveCell();
216215
}
217216

0 commit comments

Comments
 (0)