Skip to content

Commit 8932565

Browse files
committed
fix(a11y): focus monitor incorrectly detecting fake mousedown from screen reader
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.
1 parent b5b3c8a commit 8932565

File tree

2 files changed

+28
-2
lines changed

2 files changed

+28
-2
lines changed

src/cdk/a11y/focus-monitor/focus-monitor.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
dispatchKeyboardEvent,
55
dispatchMouseEvent,
66
patchElementFocus,
7+
createMouseEvent,
8+
dispatchEvent,
79
} from '@angular/cdk/testing/private';
810
import {Component, NgZone} from '@angular/core';
911
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
@@ -112,6 +114,26 @@ describe('FocusMonitor', () => {
112114
expect(changeHandler).toHaveBeenCalledWith('program');
113115
}));
114116

117+
it('should detect fake mousedown from a screen reader', fakeAsync(() => {
118+
// Simulate focus via a fake mousedown from a screen reader.
119+
dispatchMouseEvent(buttonElement, 'mousedown');
120+
const event = createMouseEvent('mousedown');
121+
Object.defineProperty(event, 'buttons', {get: () => 0});
122+
dispatchEvent(buttonElement, event);
123+
124+
buttonElement.focus();
125+
fixture.detectChanges();
126+
flush();
127+
128+
expect(buttonElement.classList.length)
129+
.toBe(2, 'button should have exactly 2 focus classes');
130+
expect(buttonElement.classList.contains('cdk-focused'))
131+
.toBe(true, 'button should have cdk-focused class');
132+
expect(buttonElement.classList.contains('cdk-keyboard-focused'))
133+
.toBe(true, 'button should have cdk-keyboard-focused class');
134+
expect(changeHandler).toHaveBeenCalledWith('keyboard');
135+
}));
136+
115137
it('focusVia keyboard should simulate keyboard focus', fakeAsync(() => {
116138
focusMonitor.focusVia(buttonElement, 'keyboard');
117139
flush();

src/cdk/a11y/focus-monitor/focus-monitor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
2222
import {coerceElement} from '@angular/cdk/coercion';
2323
import {DOCUMENT} from '@angular/common';
24+
import {isFakeMousedownFromScreenReader} from '../fake-mousedown';
2425

2526

2627
// This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
@@ -99,11 +100,14 @@ export class FocusMonitor implements OnDestroy {
99100
* Event listener for `mousedown` events on the document.
100101
* Needs to be an arrow function in order to preserve the context when it gets bound.
101102
*/
102-
private _documentMousedownListener = () => {
103+
private _documentMousedownListener = (event: MouseEvent) => {
103104
// On mousedown record the origin only if there is not touch
104105
// target, since a mousedown can happen as a result of a touch event.
105106
if (!this._lastTouchTarget) {
106-
this._setOriginForCurrentEventQueue('mouse');
107+
// In some cases screen readers fire fake `mousedown` events instead of `keydown`.
108+
// Resolve the focus source to `keyboard` if we detect one of them.
109+
const source = isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse';
110+
this._setOriginForCurrentEventQueue(source);
107111
}
108112
}
109113

0 commit comments

Comments
 (0)