Skip to content

fix(material/datepicker): calendar reopening on spacebar selection #23336

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
Aug 20, 2021
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
10 changes: 10 additions & 0 deletions src/material/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ describe('MatCalendar', () => {

dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
Expand All @@ -235,6 +237,8 @@ describe('MatCalendar', () => {

dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('month');
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
Expand All @@ -258,6 +262,8 @@ describe('MatCalendar', () => {

dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
Expand All @@ -272,6 +278,8 @@ describe('MatCalendar', () => {

dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('year');
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
Expand Down Expand Up @@ -582,6 +590,8 @@ describe('MatCalendar', () => {
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
fixture.detectChanges();

expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toBeUndefined();
Expand Down
2 changes: 2 additions & 0 deletions src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ describe('MatDatepicker', () => {
flush();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
fixture.detectChanges();
flush();

expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();
Expand Down
1 change: 1 addition & 0 deletions src/material/datepicker/month-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
(selectedValueChange)="_dateSelected($event)"
(previewChange)="_previewChanged($event)"
(keyup)="_handleCalendarBodyKeyup($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
4 changes: 4 additions & 0 deletions src/material/datepicker/month-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ describe('MatMonthView', () => {

dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
fixture.detectChanges();

expect(testComponent.selected).toEqual(new Date(2017, JAN, 4));
});
Expand All @@ -299,6 +301,8 @@ describe('MatMonthView', () => {

dispatchKeyboardEvent(calendarBodyEl, 'keydown', SPACE);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keyup', SPACE);
fixture.detectChanges();

expect(testComponent.selected).toEqual(new Date(2017, JAN, 4));
});
Expand Down
28 changes: 26 additions & 2 deletions src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const DAYS_PER_WEEK = 7;
export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
private _rerenderSubscription = Subscription.EMPTY;

/** Flag used to filter out space/enter keyup events that originated outside of the view. */
private _selectionKeyPressed: boolean;

/**
* The date to display in this month view (everything other than the month and year is ignored).
*/
Expand Down Expand Up @@ -285,9 +288,14 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
break;
case ENTER:
case SPACE:
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
this._dateSelected({value: this._dateAdapter.getDate(this._activeDate), event});
this._selectionKeyPressed = true;

if (this._canSelect(this._activeDate)) {
// Prevent unexpected default actions such as form submission.
// Note that we only prevent the default action here while the selection happens in
// `keyup` below. We can't do the selection here, because it can cause the calendar to
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
// because it's too late (see #23305).
event.preventDefault();
}
return;
Expand Down Expand Up @@ -315,6 +323,17 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
event.preventDefault();
}

/** Handles keyup events on the calendar body when calendar is in month view. */
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
if (event.keyCode === SPACE || event.keyCode === ENTER) {
if (this._selectionKeyPressed && this._canSelect(this._activeDate)) {
this._dateSelected({value: this._dateAdapter.getDate(this._activeDate), event});
}

this._selectionKeyPressed = false;
}
}

/** Initializes this month view. */
_init() {
this._setRanges(this.selected);
Expand Down Expand Up @@ -450,4 +469,9 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
this._comparisonRangeStart = this._getCellCompareValue(this.comparisonStart);
this._comparisonRangeEnd = this._getCellCompareValue(this.comparisonEnd);
}

/** Gets whether a date can be selected in the month view. */
private _canSelect(date: D) {
return !this.dateFilter || this.dateFilter(date);
}
}
1 change: 1 addition & 0 deletions src/material/datepicker/multi-year-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[cellAspectRatio]="4 / 7"
[activeCell]="_getActiveCell()"
(selectedValueChange)="_yearSelected($event)"
(keyup)="_handleCalendarBodyKeyup($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
20 changes: 19 additions & 1 deletion src/material/datepicker/multi-year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export const yearsPerRow = 4;
export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
private _rerenderSubscription = Subscription.EMPTY;

/** Flag used to filter out space/enter keyup events that originated outside of the view. */
private _selectionKeyPressed: boolean;

/** The date to display in this multi-year view (everything other than the year is ignored). */
@Input()
get activeDate(): D { return this._activeDate; }
Expand Down Expand Up @@ -233,7 +236,11 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
break;
case ENTER:
case SPACE:
this._yearSelected({value: this._dateAdapter.getYear(this._activeDate), event});
// Note that we only prevent the default action here while the selection happens in
// `keyup` below. We can't do the selection here, because it can cause the calendar to
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
// because it's too late (see #23305).
this._selectionKeyPressed = true;
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
Expand All @@ -248,6 +255,17 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
event.preventDefault();
}

/** Handles keyup events on the calendar body when calendar is in multi-year view. */
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
if (event.keyCode === SPACE || event.keyCode === ENTER) {
if (this._selectionKeyPressed) {
this._yearSelected({value: this._dateAdapter.getYear(this._activeDate), event});
}

this._selectionKeyPressed = false;
}
}

_getActiveCell(): number {
return getActiveOffset(this._dateAdapter, this.activeDate, this.minDate, this.maxDate);
}
Expand Down
1 change: 1 addition & 0 deletions src/material/datepicker/year-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[cellAspectRatio]="4 / 7"
[activeCell]="_dateAdapter.getMonth(activeDate)"
(selectedValueChange)="_monthSelected($event)"
(keyup)="_handleCalendarBodyKeyup($event)"
(keydown)="_handleCalendarBodyKeydown($event)">
</tbody>
</table>
20 changes: 19 additions & 1 deletion src/material/datepicker/year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ import {DateRange} from './date-selection-model';
export class MatYearView<D> implements AfterContentInit, OnDestroy {
private _rerenderSubscription = Subscription.EMPTY;

/** Flag used to filter out space/enter keyup events that originated outside of the view. */
private _selectionKeyPressed: boolean;

/** The date to display in this year view (everything other than the year is ignored). */
@Input()
get activeDate(): D { return this._activeDate; }
Expand Down Expand Up @@ -220,7 +223,11 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
break;
case ENTER:
case SPACE:
this._monthSelected({value: this._dateAdapter.getMonth(this._activeDate), event});
// Note that we only prevent the default action here while the selection happens in
// `keyup` below. We can't do the selection here, because it can cause the calendar to
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
// because it's too late (see #23305).
this._selectionKeyPressed = true;
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
Expand All @@ -236,6 +243,17 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
event.preventDefault();
}

/** Handles keyup events on the calendar body when calendar is in year view. */
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
if (event.keyCode === SPACE || event.keyCode === ENTER) {
if (this._selectionKeyPressed) {
this._monthSelected({value: this._dateAdapter.getMonth(this._activeDate), event});
}

this._selectionKeyPressed = false;
}
}

/** Initializes this year view. */
_init() {
this._setSelectedMonth(this.selected);
Expand Down
3 changes: 3 additions & 0 deletions tools/public_api_guard/material/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
_firstWeekOffset: number;
_focusActiveCell(movePreview?: boolean): void;
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
_init(): void;
_isRange: boolean;
_matCalendarBody: MatCalendarBody;
Expand Down Expand Up @@ -834,6 +835,7 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
// (undocumented)
_getActiveCell(): number;
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
_init(): void;
_matCalendarBody: MatCalendarBody;
get maxDate(): D | null;
Expand Down Expand Up @@ -922,6 +924,7 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
dateFilter: (date: D) => boolean;
_focusActiveCell(): void;
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
_init(): void;
_matCalendarBody: MatCalendarBody;
get maxDate(): D | null;
Expand Down