Skip to content

Commit

Permalink
fix(material/datepicker): calendar reopening on spacebar selection (#…
Browse files Browse the repository at this point in the history
…23336)

Fixes that the Material calendar was reopening immediately when a date is selected using the spacebar on Firefox.

Fixes #23305.
  • Loading branch information
crisbeto committed Aug 20, 2021
1 parent 3dc5af4 commit b761dbc
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 4 deletions.
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

0 comments on commit b761dbc

Please sign in to comment.