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); } }