From 695dde64abde2b16015c46a7681ab4c9dd8c2f44 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 10 Apr 2020 00:21:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(a11y):=20focus=20monitor=20incorrectly=20de?= =?UTF-8?q?tecting=20fake=20mousedown=E2=80=A6=20(#15214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some cases screen readers dispatch a fake `mousedown` event, instead of a `keydown` which causes the `FocusMonitor` to register focus as if it's coming from a keyboard interaction. An example where this is visible is in the `mat-datepicker` calendar where having screen reader on and opening the calendar with the keyboard won't show the focus indication, whereas it's visible if the screen reader is turned off. These changes add an extra check that will detect fake mousedown events correctly. --- .../a11y/focus-monitor/focus-monitor.spec.ts | 22 +++++++++++++++++++ src/cdk/a11y/focus-monitor/focus-monitor.ts | 8 +++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts b/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts index 35892f15b6f9..a3350ba97dc2 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.spec.ts @@ -4,6 +4,8 @@ import { dispatchKeyboardEvent, dispatchMouseEvent, patchElementFocus, + createMouseEvent, + dispatchEvent, } from '@angular/cdk/testing/private'; import {Component, NgZone} from '@angular/core'; import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing'; @@ -118,6 +120,26 @@ describe('FocusMonitor', () => { expect(changeHandler).toHaveBeenCalledWith('program'); })); + it('should detect fake mousedown from a screen reader', fakeAsync(() => { + // Simulate focus via a fake mousedown from a screen reader. + dispatchMouseEvent(buttonElement, 'mousedown'); + const event = createMouseEvent('mousedown'); + Object.defineProperty(event, 'buttons', {get: () => 0}); + dispatchEvent(buttonElement, event); + + buttonElement.focus(); + fixture.detectChanges(); + flush(); + + expect(buttonElement.classList.length) + .toBe(2, 'button should have exactly 2 focus classes'); + expect(buttonElement.classList.contains('cdk-focused')) + .toBe(true, 'button should have cdk-focused class'); + expect(buttonElement.classList.contains('cdk-keyboard-focused')) + .toBe(true, 'button should have cdk-keyboard-focused class'); + expect(changeHandler).toHaveBeenCalledWith('keyboard'); + })); + it('focusVia keyboard should simulate keyboard focus', fakeAsync(() => { focusMonitor.focusVia(buttonElement, 'keyboard'); flush(); diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 2cb550f1d01f..d396dff40081 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -22,6 +22,7 @@ import { import {Observable, of as observableOf, Subject, Subscription} from 'rxjs'; import {coerceElement} from '@angular/cdk/coercion'; import {DOCUMENT} from '@angular/common'; +import {isFakeMousedownFromScreenReader} from '../fake-mousedown'; // This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found @@ -129,11 +130,14 @@ export class FocusMonitor implements OnDestroy { * Event listener for `mousedown` events on the document. * Needs to be an arrow function in order to preserve the context when it gets bound. */ - private _documentMousedownListener = () => { + private _documentMousedownListener = (event: MouseEvent) => { // On mousedown record the origin only if there is not touch // target, since a mousedown can happen as a result of a touch event. if (!this._lastTouchTarget) { - this._setOriginForCurrentEventQueue('mouse'); + // In some cases screen readers fire fake `mousedown` events instead of `keydown`. + // Resolve the focus source to `keyboard` if we detect one of them. + const source = isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse'; + this._setOriginForCurrentEventQueue(source); } }