Skip to content

Commit 1244e25

Browse files
crisbetoamysorto
authored andcommitted
fix(material/datepicker): calendar reopening on spacebar selection (#23336)
Fixes that the Material calendar was reopening immediately when a date is selected using the spacebar on Firefox. Fixes #23305. (cherry picked from commit b761dbc)
1 parent 3bf50c8 commit 1244e25

File tree

10 files changed

+86
-4
lines changed

10 files changed

+86
-4
lines changed

src/material/datepicker/calendar.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ describe('MatCalendar', () => {
221221

222222
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
223223
fixture.detectChanges();
224+
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
225+
fixture.detectChanges();
224226

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

236238
dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
237239
fixture.detectChanges();
240+
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
241+
fixture.detectChanges();
238242

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

259263
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
260264
fixture.detectChanges();
265+
dispatchKeyboardEvent(tableBodyEl, 'keyup', ENTER);
266+
fixture.detectChanges();
261267

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

273279
dispatchKeyboardEvent(tableBodyEl, 'keydown', SPACE);
274280
fixture.detectChanges();
281+
dispatchKeyboardEvent(tableBodyEl, 'keyup', SPACE);
282+
fixture.detectChanges();
275283

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

586596
expect(calendarInstance.currentView).toBe('month');
587597
expect(testComponent.selected).toBeUndefined();

src/material/datepicker/datepicker.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ describe('MatDatepicker', () => {
324324
flush();
325325
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
326326
fixture.detectChanges();
327+
dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
328+
fixture.detectChanges();
327329
flush();
328330

329331
expect(document.querySelector('.mat-datepicker-dialog')).toBeNull();

src/material/datepicker/month-view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
2626
(selectedValueChange)="_dateSelected($event)"
2727
(previewChange)="_previewChanged($event)"
28+
(keyup)="_handleCalendarBodyKeyup($event)"
2829
(keydown)="_handleCalendarBodyKeydown($event)">
2930
</tbody>
3031
</table>

src/material/datepicker/month-view.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ describe('MatMonthView', () => {
287287

288288
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
289289
fixture.detectChanges();
290+
dispatchKeyboardEvent(calendarBodyEl, 'keyup', ENTER);
291+
fixture.detectChanges();
290292

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

300302
dispatchKeyboardEvent(calendarBodyEl, 'keydown', SPACE);
301303
fixture.detectChanges();
304+
dispatchKeyboardEvent(calendarBodyEl, 'keyup', SPACE);
305+
fixture.detectChanges();
302306

303307
expect(testComponent.selected).toEqual(new Date(2017, JAN, 4));
304308
});

src/material/datepicker/month-view.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const DAYS_PER_WEEK = 7;
7171
export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
7272
private _rerenderSubscription = Subscription.EMPTY;
7373

74+
/** Flag used to filter out space/enter keyup events that originated outside of the view. */
75+
private _selectionKeyPressed: boolean;
76+
7477
/**
7578
* The date to display in this month view (everything other than the month and year is ignored).
7679
*/
@@ -285,9 +288,14 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
285288
break;
286289
case ENTER:
287290
case SPACE:
288-
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
289-
this._dateSelected({value: this._dateAdapter.getDate(this._activeDate), event});
291+
this._selectionKeyPressed = true;
292+
293+
if (this._canSelect(this._activeDate)) {
290294
// Prevent unexpected default actions such as form submission.
295+
// Note that we only prevent the default action here while the selection happens in
296+
// `keyup` below. We can't do the selection here, because it can cause the calendar to
297+
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
298+
// because it's too late (see #23305).
291299
event.preventDefault();
292300
}
293301
return;
@@ -315,6 +323,17 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
315323
event.preventDefault();
316324
}
317325

326+
/** Handles keyup events on the calendar body when calendar is in month view. */
327+
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
328+
if (event.keyCode === SPACE || event.keyCode === ENTER) {
329+
if (this._selectionKeyPressed && this._canSelect(this._activeDate)) {
330+
this._dateSelected({value: this._dateAdapter.getDate(this._activeDate), event});
331+
}
332+
333+
this._selectionKeyPressed = false;
334+
}
335+
}
336+
318337
/** Initializes this month view. */
319338
_init() {
320339
this._setRanges(this.selected);
@@ -450,4 +469,9 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
450469
this._comparisonRangeStart = this._getCellCompareValue(this.comparisonStart);
451470
this._comparisonRangeEnd = this._getCellCompareValue(this.comparisonEnd);
452471
}
472+
473+
/** Gets whether a date can be selected in the month view. */
474+
private _canSelect(date: D) {
475+
return !this.dateFilter || this.dateFilter(date);
476+
}
453477
}

src/material/datepicker/multi-year-view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[cellAspectRatio]="4 / 7"
1212
[activeCell]="_getActiveCell()"
1313
(selectedValueChange)="_yearSelected($event)"
14+
(keyup)="_handleCalendarBodyKeyup($event)"
1415
(keydown)="_handleCalendarBodyKeydown($event)">
1516
</tbody>
1617
</table>

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export const yearsPerRow = 4;
6262
export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
6363
private _rerenderSubscription = Subscription.EMPTY;
6464

65+
/** Flag used to filter out space/enter keyup events that originated outside of the view. */
66+
private _selectionKeyPressed: boolean;
67+
6568
/** The date to display in this multi-year view (everything other than the year is ignored). */
6669
@Input()
6770
get activeDate(): D { return this._activeDate; }
@@ -233,7 +236,11 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
233236
break;
234237
case ENTER:
235238
case SPACE:
236-
this._yearSelected({value: this._dateAdapter.getYear(this._activeDate), event});
239+
// Note that we only prevent the default action here while the selection happens in
240+
// `keyup` below. We can't do the selection here, because it can cause the calendar to
241+
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
242+
// because it's too late (see #23305).
243+
this._selectionKeyPressed = true;
237244
break;
238245
default:
239246
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
@@ -248,6 +255,17 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
248255
event.preventDefault();
249256
}
250257

258+
/** Handles keyup events on the calendar body when calendar is in multi-year view. */
259+
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
260+
if (event.keyCode === SPACE || event.keyCode === ENTER) {
261+
if (this._selectionKeyPressed) {
262+
this._yearSelected({value: this._dateAdapter.getYear(this._activeDate), event});
263+
}
264+
265+
this._selectionKeyPressed = false;
266+
}
267+
}
268+
251269
_getActiveCell(): number {
252270
return getActiveOffset(this._dateAdapter, this.activeDate, this.minDate, this.maxDate);
253271
}

src/material/datepicker/year-view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
[cellAspectRatio]="4 / 7"
1414
[activeCell]="_dateAdapter.getMonth(activeDate)"
1515
(selectedValueChange)="_monthSelected($event)"
16+
(keyup)="_handleCalendarBodyKeyup($event)"
1617
(keydown)="_handleCalendarBodyKeydown($event)">
1718
</tbody>
1819
</table>

src/material/datepicker/year-view.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ import {DateRange} from './date-selection-model';
5959
export class MatYearView<D> implements AfterContentInit, OnDestroy {
6060
private _rerenderSubscription = Subscription.EMPTY;
6161

62+
/** Flag used to filter out space/enter keyup events that originated outside of the view. */
63+
private _selectionKeyPressed: boolean;
64+
6265
/** The date to display in this year view (everything other than the year is ignored). */
6366
@Input()
6467
get activeDate(): D { return this._activeDate; }
@@ -220,7 +223,11 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
220223
break;
221224
case ENTER:
222225
case SPACE:
223-
this._monthSelected({value: this._dateAdapter.getMonth(this._activeDate), event});
226+
// Note that we only prevent the default action here while the selection happens in
227+
// `keyup` below. We can't do the selection here, because it can cause the calendar to
228+
// reopen if focus is restored immediately. We also can't call `preventDefault` on `keyup`
229+
// because it's too late (see #23305).
230+
this._selectionKeyPressed = true;
224231
break;
225232
default:
226233
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
@@ -236,6 +243,17 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
236243
event.preventDefault();
237244
}
238245

246+
/** Handles keyup events on the calendar body when calendar is in year view. */
247+
_handleCalendarBodyKeyup(event: KeyboardEvent): void {
248+
if (event.keyCode === SPACE || event.keyCode === ENTER) {
249+
if (this._selectionKeyPressed) {
250+
this._monthSelected({value: this._dateAdapter.getMonth(this._activeDate), event});
251+
}
252+
253+
this._selectionKeyPressed = false;
254+
}
255+
}
256+
239257
/** Initializes this year view. */
240258
_init() {
241259
this._setSelectedMonth(this.selected);

tools/public_api_guard/material/datepicker.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,7 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
785785
_firstWeekOffset: number;
786786
_focusActiveCell(movePreview?: boolean): void;
787787
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
788+
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
788789
_init(): void;
789790
_isRange: boolean;
790791
_matCalendarBody: MatCalendarBody;
@@ -834,6 +835,7 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
834835
// (undocumented)
835836
_getActiveCell(): number;
836837
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
838+
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
837839
_init(): void;
838840
_matCalendarBody: MatCalendarBody;
839841
get maxDate(): D | null;
@@ -922,6 +924,7 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
922924
dateFilter: (date: D) => boolean;
923925
_focusActiveCell(): void;
924926
_handleCalendarBodyKeydown(event: KeyboardEvent): void;
927+
_handleCalendarBodyKeyup(event: KeyboardEvent): void;
925928
_init(): void;
926929
_matCalendarBody: MatCalendarBody;
927930
get maxDate(): D | null;

0 commit comments

Comments
 (0)