Skip to content

Commit 4ca4fbd

Browse files
authored
fix(material/datepicker): page scrolling for fast keyboard repeat (#24991)
In an earlier change we introduced a timeout between when the datepicker is opened and when focus is moved into the current view. This means that the browser has some time to fire another keyboard event before we start preventing their default actions from inside the calendar, potentially allowing the page to be scrolled. These changes fix the issue by always preventing the default action of navigation keys at the overlay level. Fixes #24969.
1 parent d0f803c commit 4ca4fbd

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

src/material/datepicker/datepicker-base.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@
88

99
import {Directionality} from '@angular/cdk/bidi';
1010
import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion';
11-
import {ESCAPE, hasModifierKey, UP_ARROW} from '@angular/cdk/keycodes';
11+
import {
12+
DOWN_ARROW,
13+
ESCAPE,
14+
hasModifierKey,
15+
LEFT_ARROW,
16+
PAGE_DOWN,
17+
PAGE_UP,
18+
RIGHT_ARROW,
19+
UP_ARROW,
20+
} from '@angular/cdk/keycodes';
1221
import {
1322
Overlay,
1423
OverlayConfig,
@@ -657,6 +666,25 @@ export abstract class MatDatepickerBase<
657666
this.close();
658667
});
659668

669+
// The `preventDefault` call happens inside the calendar as well, however focus moves into
670+
// it inside a timeout which can give browsers a chance to fire off a keyboard event in-between
671+
// that can scroll the page (see #24969). Always block default actions of arrow keys for the
672+
// entire overlay so the page doesn't get scrolled by accident.
673+
overlayRef.keydownEvents().subscribe(event => {
674+
const keyCode = event.keyCode;
675+
676+
if (
677+
keyCode === UP_ARROW ||
678+
keyCode === DOWN_ARROW ||
679+
keyCode === LEFT_ARROW ||
680+
keyCode === RIGHT_ARROW ||
681+
keyCode === PAGE_UP ||
682+
keyCode === PAGE_DOWN
683+
) {
684+
event.preventDefault();
685+
}
686+
});
687+
660688
this._componentRef = overlayRef.attach(portal);
661689
this._forwardContentValues(this._componentRef.instance);
662690

src/material/datepicker/datepicker.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import {Directionality} from '@angular/cdk/bidi';
2-
import {DOWN_ARROW, ENTER, ESCAPE, RIGHT_ARROW, UP_ARROW} from '@angular/cdk/keycodes';
2+
import {
3+
DOWN_ARROW,
4+
ENTER,
5+
ESCAPE,
6+
LEFT_ARROW,
7+
PAGE_DOWN,
8+
PAGE_UP,
9+
RIGHT_ARROW,
10+
UP_ARROW,
11+
} from '@angular/cdk/keycodes';
312
import {Overlay} from '@angular/cdk/overlay';
413
import {ScrollDispatcher} from '@angular/cdk/scrolling';
514
import {
@@ -605,6 +614,24 @@ describe('MatDatepicker', () => {
605614

606615
expect(document.querySelector('.mat-datepicker-content')).toBeNull();
607616
}));
617+
618+
it('should prevent the default action of navigation keys before the focus timeout has elapsed', fakeAsync(() => {
619+
testComponent.datepicker.open();
620+
fixture.detectChanges();
621+
622+
// Do the assertions before flushing the delays since we want
623+
// to check specifically what happens before they have fired.
624+
[UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, PAGE_UP, PAGE_DOWN].forEach(keyCode => {
625+
const event = dispatchKeyboardEvent(document.body, 'keydown', keyCode);
626+
fixture.detectChanges();
627+
expect(event.defaultPrevented)
628+
.withContext(`Expected default action to be prevented for key code ${keyCode}`)
629+
.toBe(true);
630+
});
631+
632+
tick();
633+
flush();
634+
}));
608635
});
609636

610637
describe('datepicker with too many inputs', () => {

0 commit comments

Comments
 (0)