Skip to content

Commit e8b28fa

Browse files
authored
fix(material/datepicker): focus lost when hitting the end of calendar (#31572)
If the user hits the end of the calendar (e.g. when a min/max date is set), we disable the calendar button which causes focus to be returned to the `body`. These changes use `disabledInteractive` on the buttons so that focus stays on the button.
1 parent 5bd42ea commit e8b28fa

File tree

4 files changed

+38
-26
lines changed

4 files changed

+38
-26
lines changed

src/material/datepicker/calendar-header.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020

2121
<button matIconButton type="button" class="mat-calendar-previous-button"
2222
[disabled]="!previousEnabled()" (click)="previousClicked()"
23-
[attr.aria-label]="prevButtonLabel">
23+
[attr.aria-label]="prevButtonLabel" disabledInteractive>
2424
<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
2525
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
2626
</svg>
2727
</button>
2828

2929
<button matIconButton type="button" class="mat-calendar-next-button"
3030
[disabled]="!nextEnabled()" (click)="nextClicked()"
31-
[attr.aria-label]="nextButtonLabel">
31+
[attr.aria-label]="nextButtonLabel" disabledInteractive>
3232
<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
3333
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
3434
</svg>

src/material/datepicker/calendar-header.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ describe('MatCalendarHeader', () => {
246246
fixture.detectChanges();
247247

248248
expect(calendarInstance.currentView).toBe('multi-year');
249-
expect(prevButton.disabled).toBe(true);
249+
expect(prevButton.getAttribute('aria-disabled')).toBe('true');
250250
});
251251

252252
it('should enable the page after the one showing minDate', () => {
@@ -255,7 +255,7 @@ describe('MatCalendarHeader', () => {
255255
fixture.detectChanges();
256256

257257
expect(calendarInstance.currentView).toBe('multi-year');
258-
expect(nextButton.disabled).toBe(false);
258+
expect(nextButton.hasAttribute('aria-disabled')).toBe(false);
259259
});
260260
});
261261

@@ -300,7 +300,7 @@ describe('MatCalendarHeader', () => {
300300
fixture.detectChanges();
301301

302302
expect(calendarInstance.currentView).toBe('multi-year');
303-
expect(nextButton.disabled).toBe(true);
303+
expect(nextButton.getAttribute('aria-disabled')).toBe('true');
304304
});
305305

306306
it('should enable the page before the one showing maxDate', () => {
@@ -309,7 +309,7 @@ describe('MatCalendarHeader', () => {
309309
fixture.detectChanges();
310310

311311
expect(calendarInstance.currentView).toBe('multi-year');
312-
expect(prevButton.disabled).toBe(false);
312+
expect(prevButton.hasAttribute('aria-disabled')).toBe(false);
313313
});
314314
});
315315

@@ -356,7 +356,7 @@ describe('MatCalendarHeader', () => {
356356
fixture.detectChanges();
357357

358358
expect(calendarInstance.currentView).toBe('multi-year');
359-
expect(nextButton.disabled).toBe(true);
359+
expect(nextButton.getAttribute('aria-disabled')).toBe('true');
360360
});
361361

362362
it('should disable the page before the one showing minDate', () => {
@@ -371,7 +371,7 @@ describe('MatCalendarHeader', () => {
371371
fixture.detectChanges();
372372

373373
expect(calendarInstance.activeDate).toEqual(new Date(2018 - yearsPerPage, JAN, 1));
374-
expect(prevButton.disabled).toBe(true);
374+
expect(prevButton.getAttribute('aria-disabled')).toBe('true');
375375
});
376376
});
377377
});

src/material/datepicker/calendar.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,13 +354,17 @@ describe('MatCalendar', () => {
354354
'.mat-calendar-previous-button',
355355
) as HTMLButtonElement;
356356

357-
expect(prevButton.disabled).withContext('previous button should not be disabled').toBe(false);
357+
expect(prevButton.hasAttribute('aria-disabled'))
358+
.withContext('previous button should not be disabled')
359+
.toBe(false);
358360
expect(calendarInstance.activeDate).toEqual(new Date(2016, FEB, 1));
359361

360362
prevButton.click();
361363
fixture.detectChanges();
362364

363-
expect(prevButton.disabled).withContext('previous button should be disabled').toBe(true);
365+
expect(prevButton.getAttribute('aria-disabled'))
366+
.withContext('previous button should be disabled')
367+
.toBe('true');
364368
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
365369

366370
prevButton.click();
@@ -378,13 +382,17 @@ describe('MatCalendar', () => {
378382
'.mat-calendar-next-button',
379383
) as HTMLButtonElement;
380384

381-
expect(nextButton.disabled).withContext('next button should not be disabled').toBe(false);
385+
expect(nextButton.hasAttribute('aria-disabled'))
386+
.withContext('next button should not be disabled')
387+
.toBe(false);
382388
expect(calendarInstance.activeDate).toEqual(new Date(2017, DEC, 1));
383389

384390
nextButton.click();
385391
fixture.detectChanges();
386392

387-
expect(nextButton.disabled).withContext('next button should be disabled').toBe(true);
393+
expect(nextButton.getAttribute('aria-disabled'))
394+
.withContext('next button should be disabled')
395+
.toBe('true');
388396
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
389397

390398
nextButton.click();

src/material/datepicker/calendar.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,28 @@ export class MatCalendarHeader<D> {
114114

115115
/** Handles user clicks on the previous button. */
116116
previousClicked(): void {
117-
this.calendar.activeDate =
118-
this.calendar.currentView == 'month'
119-
? this._dateAdapter.addCalendarMonths(this.calendar.activeDate, -1)
120-
: this._dateAdapter.addCalendarYears(
121-
this.calendar.activeDate,
122-
this.calendar.currentView == 'year' ? -1 : -yearsPerPage,
123-
);
117+
if (this.previousEnabled()) {
118+
this.calendar.activeDate =
119+
this.calendar.currentView == 'month'
120+
? this._dateAdapter.addCalendarMonths(this.calendar.activeDate, -1)
121+
: this._dateAdapter.addCalendarYears(
122+
this.calendar.activeDate,
123+
this.calendar.currentView == 'year' ? -1 : -yearsPerPage,
124+
);
125+
}
124126
}
125127

126128
/** Handles user clicks on the next button. */
127129
nextClicked(): void {
128-
this.calendar.activeDate =
129-
this.calendar.currentView == 'month'
130-
? this._dateAdapter.addCalendarMonths(this.calendar.activeDate, 1)
131-
: this._dateAdapter.addCalendarYears(
132-
this.calendar.activeDate,
133-
this.calendar.currentView == 'year' ? 1 : yearsPerPage,
134-
);
130+
if (this.nextEnabled()) {
131+
this.calendar.activeDate =
132+
this.calendar.currentView == 'month'
133+
? this._dateAdapter.addCalendarMonths(this.calendar.activeDate, 1)
134+
: this._dateAdapter.addCalendarYears(
135+
this.calendar.activeDate,
136+
this.calendar.currentView == 'year' ? 1 : yearsPerPage,
137+
);
138+
}
135139
}
136140

137141
/** Whether the previous period button is enabled. */

0 commit comments

Comments
 (0)