mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(datepicker): don't autofocus calendar cell if used outside of overlay (#11049)
This commit is contained in:
committed by
Joey Perrott
parent
860ce1324f
commit
28977976e3
@@ -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';
|
||||
@@ -14,6 +19,7 @@ import {MatDatepickerModule} from './datepicker-module';
|
||||
|
||||
describe('MatCalendar', () => {
|
||||
let dir: {value: Direction};
|
||||
let zone: MockNgZone;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -29,6 +35,7 @@ describe('MatCalendar', () => {
|
||||
],
|
||||
providers: [
|
||||
MatDatepickerIntl,
|
||||
{provide: NgZone, useFactory: () => zone = new MockNgZone()},
|
||||
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
|
||||
],
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';
|
||||
import {
|
||||
AfterContentInit,
|
||||
AfterViewChecked,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -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,
|
||||
@@ -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>;
|
||||
|
||||
@@ -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; }
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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() {
|
||||
@@ -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();
|
||||
@@ -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)) {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -100,7 +100,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;
|
||||
@@ -141,17 +141,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() {
|
||||
|
||||
@@ -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;
|
||||
@@ -155,7 +155,6 @@ export class MatMonthView<D> implements AfterContentInit {
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init();
|
||||
this._focusActiveCell();
|
||||
}
|
||||
|
||||
/** Handles when a new date is selected. */
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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[][];
|
||||
@@ -125,7 +125,6 @@ export class MatMultiYearView<D> implements AfterContentInit {
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init();
|
||||
this._focusActiveCell();
|
||||
}
|
||||
|
||||
/** Initializes this multi-year view. */
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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[][];
|
||||
@@ -130,7 +130,6 @@ export class MatYearView<D> implements AfterContentInit {
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init();
|
||||
this._focusActiveCell();
|
||||
}
|
||||
|
||||
/** Handles when a new month is selected. */
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user