Skip to content

Commit 5188119

Browse files
committed
fix(material/datepicker): fix Voiceover losing focus on PageDown
Fixes an issue where Voiceover loses focus when pressing PageDown/PageUp in the calendar to go to the next month/year (issue #24330). Adding a 20ms timeout seems to fix this. Note that this will not fully fix the issue until #24397 is merged. Address #24330.
1 parent ef98cd8 commit 5188119

File tree

6 files changed

+175
-60
lines changed

6 files changed

+175
-60
lines changed

src/material/datepicker/calendar-body.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
OnDestroy,
2121
AfterViewChecked,
2222
} from '@angular/core';
23-
import {take} from 'rxjs/operators';
23+
import {delay, take} from 'rxjs/operators';
2424

2525
/** Extra CSS classes that can be associated with a calendar cell. */
2626
export type MatCalendarCellCssClasses = string | string[] | Set<string> | {[key: string]: any};
@@ -31,6 +31,8 @@ export type MatCalendarCellClassFunction<D> = (
3131
view: 'month' | 'year' | 'multi-year',
3232
) => MatCalendarCellCssClasses;
3333

34+
export const FOCUS_ACTIVE_CELL_DELAY = 20;
35+
3436
/**
3537
* An internal class that represents the data corresponding to a single calendar cell.
3638
* @docs-private
@@ -216,10 +218,14 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
216218
return cellNumber == this.activeCell;
217219
}
218220

219-
/** Focuses the active cell after the microtask queue is empty. */
221+
/**
222+
* Focuses the active cell after the microtask queue is empty.
223+
*
224+
* Adds a 20ms delay to fix Voiceover losing focus when pressing PageUp/PageDown (issue #24330).
225+
*/
220226
_focusActiveCell(movePreview = true) {
221227
this._ngZone.runOutsideAngular(() => {
222-
this._ngZone.onStable.pipe(take(1)).subscribe(() => {
228+
this._ngZone.onStable.pipe(take(1), delay(FOCUS_ACTIVE_CELL_DELAY)).subscribe(() => {
223229
const activeCell: HTMLElement | null = this._elementRef.nativeElement.querySelector(
224230
'.mat-calendar-body-active',
225231
);

src/material/datepicker/calendar.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ import {
77
MockNgZone,
88
} from '../../cdk/testing/private';
99
import {Component, NgZone} from '@angular/core';
10-
import {waitForAsync, ComponentFixture, inject, TestBed} from '@angular/core/testing';
10+
import {
11+
fakeAsync,
12+
waitForAsync,
13+
ComponentFixture,
14+
inject,
15+
TestBed,
16+
tick,
17+
} from '@angular/core/testing';
1118
import {DateAdapter, MatNativeDateModule} from '@angular/material/core';
1219
import {DEC, FEB, JAN, JUL, NOV} from '../testing';
1320
import {By} from '@angular/platform-browser';
1421
import {MatCalendar} from './calendar';
1522
import {MatDatepickerIntl} from './datepicker-intl';
1623
import {MatDatepickerModule} from './datepicker-module';
24+
import {FOCUS_ACTIVE_CELL_DELAY} from './calendar-body';
1725

1826
describe('MatCalendar', () => {
1927
let zone: MockNgZone;
@@ -190,7 +198,7 @@ describe('MatCalendar', () => {
190198
expect(activeCell.focus).not.toHaveBeenCalled();
191199
});
192200

193-
it('should move focus to the active cell when the view changes', () => {
201+
it('should move focus to the active cell when the view changes', fakeAsync(() => {
194202
calendarInstance.currentView = 'multi-year';
195203
fixture.detectChanges();
196204

@@ -200,9 +208,10 @@ describe('MatCalendar', () => {
200208
spyOn(activeCell, 'focus').and.callThrough();
201209

202210
zone.simulateZoneExit();
211+
tick(FOCUS_ACTIVE_CELL_DELAY);
203212

204213
expect(activeCell.focus).toHaveBeenCalled();
205-
});
214+
}));
206215

207216
describe('year view', () => {
208217
beforeEach(() => {

src/material/datepicker/date-range-input.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {MatDateRangeInput} from './date-range-input';
2222
import {MatDateRangePicker} from './date-range-picker';
2323
import {MatStartDate, MatEndDate} from './date-range-input-parts';
2424
import {Subscription} from 'rxjs';
25+
import {FOCUS_ACTIVE_CELL_DELAY} from './calendar-body';
2526

2627
describe('MatDateRangeInput', () => {
2728
function createComponent<T>(
@@ -206,7 +207,7 @@ describe('MatDateRangeInput', () => {
206207

207208
fixture.componentInstance.rangePicker.open();
208209
fixture.detectChanges();
209-
tick();
210+
tick(FOCUS_ACTIVE_CELL_DELAY);
210211

211212
const popup = document.querySelector('.cdk-overlay-pane .mat-datepicker-content-container')!;
212213
expect(popup).toBeTruthy();
@@ -526,7 +527,7 @@ describe('MatDateRangeInput', () => {
526527

527528
fixture.componentInstance.rangePicker.open();
528529
fixture.detectChanges();
529-
tick();
530+
tick(FOCUS_ACTIVE_CELL_DELAY);
530531

531532
const rangeTexts = Array.from(
532533
overlayContainerElement!.querySelectorAll(
@@ -559,7 +560,7 @@ describe('MatDateRangeInput', () => {
559560

560561
fixture.componentInstance.rangePicker.open();
561562
fixture.detectChanges();
562-
tick();
563+
tick(FOCUS_ACTIVE_CELL_DELAY);
563564

564565
const rangeTexts = Array.from(
565566
overlayContainerElement!.querySelectorAll(
@@ -675,6 +676,7 @@ describe('MatDateRangeInput', () => {
675676

676677
rangePicker.open();
677678
fixture.detectChanges();
679+
tick(FOCUS_ACTIVE_CELL_DELAY);
678680
flush();
679681

680682
expect(startModel.dirty).toBe(false);
@@ -902,7 +904,7 @@ describe('MatDateRangeInput', () => {
902904

903905
fixture.componentInstance.rangePicker.open();
904906
fixture.detectChanges();
905-
tick();
907+
tick(FOCUS_ACTIVE_CELL_DELAY);
906908

907909
const fromDate = new Date(2020, 0, 1);
908910
const toDate = new Date(2020, 0, 2);
@@ -930,7 +932,7 @@ describe('MatDateRangeInput', () => {
930932

931933
fixture.componentInstance.rangePicker.open();
932934
fixture.detectChanges();
933-
tick();
935+
tick(FOCUS_ACTIVE_CELL_DELAY);
934936

935937
const fromDate2 = new Date(2021, 0, 1);
936938
const toDate2 = new Date(2021, 0, 2);

src/material/datepicker/datepicker-actions.spec.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
2-
import {ComponentFixture, TestBed, flush, fakeAsync} from '@angular/core/testing';
2+
import {ComponentFixture, TestBed, flush, fakeAsync, tick} from '@angular/core/testing';
33
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
44
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
55
import {MatNativeDateModule} from '@angular/material/core';
@@ -8,6 +8,7 @@ import {MatInputModule} from '@angular/material/input';
88
import {CommonModule} from '@angular/common';
99
import {MatDatepickerModule} from './datepicker-module';
1010
import {MatDatepicker} from './datepicker';
11+
import {FOCUS_ACTIVE_CELL_DELAY} from './calendar-body';
1112

1213
describe('MatDatepickerActions', () => {
1314
function createComponent<T>(component: Type<T>): ComponentFixture<T> {
@@ -28,37 +29,42 @@ describe('MatDatepickerActions', () => {
2829
return TestBed.createComponent(component);
2930
}
3031

31-
it('should render the actions inside calendar panel in popup mode', () => {
32+
it('should render the actions inside calendar panel in popup mode', fakeAsync(() => {
3233
const fixture = createComponent(DatepickerWithActions);
3334
fixture.detectChanges();
3435
fixture.componentInstance.datepicker.open();
3536
fixture.detectChanges();
37+
tick(FOCUS_ACTIVE_CELL_DELAY);
38+
flush();
3639

3740
const actions = document.querySelector('.mat-datepicker-content .mat-datepicker-actions');
3841
expect(actions).toBeTruthy();
3942
expect(actions?.querySelector('.cancel')).toBeTruthy();
4043
expect(actions?.querySelector('.apply')).toBeTruthy();
41-
});
44+
}));
4245

43-
it('should render the actions inside calendar panel in touch UI mode', () => {
46+
it('should render the actions inside calendar panel in touch UI mode', fakeAsync(() => {
4447
const fixture = createComponent(DatepickerWithActions);
4548
fixture.componentInstance.touchUi = true;
4649
fixture.detectChanges();
4750
fixture.componentInstance.datepicker.open();
4851
fixture.detectChanges();
52+
tick(FOCUS_ACTIVE_CELL_DELAY);
53+
flush();
4954

5055
const actions = document.querySelector('.mat-datepicker-content .mat-datepicker-actions');
5156
expect(actions).toBeTruthy();
5257
expect(actions?.querySelector('.cancel')).toBeTruthy();
5358
expect(actions?.querySelector('.apply')).toBeTruthy();
54-
});
59+
}));
5560

5661
it('should not assign the value or close the datepicker when a value is selected', fakeAsync(() => {
5762
const fixture = createComponent(DatepickerWithActions);
5863
fixture.detectChanges();
5964
const {control, datepicker, onDateChange, input} = fixture.componentInstance;
6065
datepicker.open();
6166
fixture.detectChanges();
67+
tick(FOCUS_ACTIVE_CELL_DELAY);
6268

6369
const content = document.querySelector('.mat-datepicker-content')!;
6470
const cells = content.querySelectorAll<HTMLElement>('.mat-calendar-body-cell');
@@ -86,6 +92,8 @@ describe('MatDatepickerActions', () => {
8692
const {control, datepicker, onDateChange, input} = fixture.componentInstance;
8793
datepicker.open();
8894
fixture.detectChanges();
95+
tick(FOCUS_ACTIVE_CELL_DELAY);
96+
flush();
8997

9098
const content = document.querySelector('.mat-datepicker-content')!;
9199
const cells = content.querySelectorAll<HTMLElement>('.mat-calendar-body-cell');
@@ -98,6 +106,7 @@ describe('MatDatepickerActions', () => {
98106

99107
cells[10].click();
100108
fixture.detectChanges();
109+
tick(FOCUS_ACTIVE_CELL_DELAY);
101110
flush();
102111

103112
expect(datepicker.opened).toBe(true);
@@ -125,6 +134,8 @@ describe('MatDatepickerActions', () => {
125134
fixture.detectChanges();
126135
datepicker.open();
127136
fixture.detectChanges();
137+
tick(FOCUS_ACTIVE_CELL_DELAY);
138+
flush();
128139

129140
const content = document.querySelector('.mat-datepicker-content')!;
130141
const cells = content.querySelectorAll<HTMLElement>('.mat-calendar-body-cell');
@@ -135,6 +146,7 @@ describe('MatDatepickerActions', () => {
135146

136147
cells[10].click();
137148
fixture.detectChanges();
149+
tick(FOCUS_ACTIVE_CELL_DELAY);
138150
flush();
139151

140152
expect(datepicker.opened).toBe(true);
@@ -156,6 +168,8 @@ describe('MatDatepickerActions', () => {
156168
const {control, datepicker, onDateChange, input} = fixture.componentInstance;
157169
datepicker.open();
158170
fixture.detectChanges();
171+
tick(FOCUS_ACTIVE_CELL_DELAY);
172+
flush();
159173

160174
const content = document.querySelector('.mat-datepicker-content')!;
161175
const cells = content.querySelectorAll<HTMLElement>('.mat-calendar-body-cell');
@@ -168,6 +182,7 @@ describe('MatDatepickerActions', () => {
168182

169183
cells[10].click();
170184
fixture.detectChanges();
185+
tick(FOCUS_ACTIVE_CELL_DELAY);
171186
flush();
172187

173188
expect(datepicker.opened).toBe(true);
@@ -192,6 +207,8 @@ describe('MatDatepickerActions', () => {
192207
const {control, datepicker, onDateChange} = fixture.componentInstance;
193208
datepicker.open();
194209
fixture.detectChanges();
210+
tick(FOCUS_ACTIVE_CELL_DELAY);
211+
flush();
195212

196213
let content = document.querySelector('.mat-datepicker-content')!;
197214
let actions = content.querySelector('.mat-datepicker-actions')!;
@@ -204,6 +221,7 @@ describe('MatDatepickerActions', () => {
204221

205222
cells[10].click();
206223
fixture.detectChanges();
224+
tick(FOCUS_ACTIVE_CELL_DELAY);
207225
flush();
208226

209227
expect(datepicker.opened).toBe(true);
@@ -233,6 +251,7 @@ describe('MatDatepickerActions', () => {
233251

234252
cells[10].click();
235253
fixture.detectChanges();
254+
tick(FOCUS_ACTIVE_CELL_DELAY);
236255
flush();
237256

238257
expect(datepicker.opened).toBe(false);

0 commit comments

Comments
 (0)