From 2443607fbf0cd3fb61deb3c3452d34b7f4d6ad15 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 1 Jun 2022 19:23:26 +0200 Subject: [PATCH] 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. (cherry picked from commit 4ca4fbd02accda7d78b762eb1fc76f2871b1c2db) --- src/material/datepicker/datepicker-base.ts | 30 +++++++++++++++++++++- src/material/datepicker/datepicker.spec.ts | 29 ++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index d09208929e90..04bbfcd7c59b 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -8,7 +8,16 @@ import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion'; -import {ESCAPE, hasModifierKey, UP_ARROW} from '@angular/cdk/keycodes'; +import { + DOWN_ARROW, + ESCAPE, + hasModifierKey, + LEFT_ARROW, + PAGE_DOWN, + PAGE_UP, + RIGHT_ARROW, + UP_ARROW, +} from '@angular/cdk/keycodes'; import { Overlay, OverlayConfig, @@ -657,6 +666,25 @@ export abstract class MatDatepickerBase< this.close(); }); + // The `preventDefault` call happens inside the calendar as well, however focus moves into + // it inside a timeout which can give browsers a chance to fire off a keyboard event in-between + // that can scroll the page (see #24969). Always block default actions of arrow keys for the + // entire overlay so the page doesn't get scrolled by accident. + overlayRef.keydownEvents().subscribe(event => { + const keyCode = event.keyCode; + + if ( + keyCode === UP_ARROW || + keyCode === DOWN_ARROW || + keyCode === LEFT_ARROW || + keyCode === RIGHT_ARROW || + keyCode === PAGE_UP || + keyCode === PAGE_DOWN + ) { + event.preventDefault(); + } + }); + this._componentRef = overlayRef.attach(portal); this._forwardContentValues(this._componentRef.instance); diff --git a/src/material/datepicker/datepicker.spec.ts b/src/material/datepicker/datepicker.spec.ts index accef0d8ee6d..035cc5d45171 100644 --- a/src/material/datepicker/datepicker.spec.ts +++ b/src/material/datepicker/datepicker.spec.ts @@ -1,5 +1,14 @@ import {Directionality} from '@angular/cdk/bidi'; -import {DOWN_ARROW, ENTER, ESCAPE, RIGHT_ARROW, UP_ARROW} from '@angular/cdk/keycodes'; +import { + DOWN_ARROW, + ENTER, + ESCAPE, + LEFT_ARROW, + PAGE_DOWN, + PAGE_UP, + RIGHT_ARROW, + UP_ARROW, +} from '@angular/cdk/keycodes'; import {Overlay} from '@angular/cdk/overlay'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import { @@ -605,6 +614,24 @@ describe('MatDatepicker', () => { expect(document.querySelector('.mat-datepicker-content')).toBeNull(); })); + + it('should prevent the default action of navigation keys before the focus timeout has elapsed', fakeAsync(() => { + testComponent.datepicker.open(); + fixture.detectChanges(); + + // Do the assertions before flushing the delays since we want + // to check specifically what happens before they have fired. + [UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, PAGE_UP, PAGE_DOWN].forEach(keyCode => { + const event = dispatchKeyboardEvent(document.body, 'keydown', keyCode); + fixture.detectChanges(); + expect(event.defaultPrevented) + .withContext(`Expected default action to be prevented for key code ${keyCode}`) + .toBe(true); + }); + + tick(); + flush(); + })); }); describe('datepicker with too many inputs', () => {