mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(FocusOriginMonitor): add support for touch events (#3020)
* added tests * add touch support to FocusOriginMonitor * add tests * added comment about 650ms * switch to comparing event targets instead of checking focus -> blur. * Add comment explaining drawbacks of how check if the focus was caused by touch. * add doc comment
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<button (click)="b.focus()">focus programmatically</button>
|
||||
|
||||
<button (click)="fom.focusVia(b, renderer, 'mouse')">focusVia: mouse</button>
|
||||
<button (click)="fom.focusVia(b, renderer, 'touch')">focusVia: touch</button>
|
||||
<button (click)="fom.focusVia(b, renderer, 'keyboard')">focusVia: keyboard</button>
|
||||
<button (click)="fom.focusVia(b, renderer, 'program')">focusVia: program</button>
|
||||
|
||||
|
||||
@@ -13,3 +13,7 @@
|
||||
.demo-button.cdk-program-focused {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
.demo-button.cdk-touch-focused {
|
||||
background: purple;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Component, Renderer, ViewChild} from '@angular/core';
|
||||
import {StyleModule} from './index';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {TAB} from '../keyboard/keycodes';
|
||||
import {FocusOriginMonitor, FocusOrigin, CdkFocusClasses} from './focus-classes';
|
||||
import {FocusOriginMonitor, FocusOrigin, CdkFocusClasses, TOUCH_BUFFER_MS} from './focus-classes';
|
||||
|
||||
describe('FocusOriginMonitor', () => {
|
||||
let fixture: ComponentFixture<PlainButton>;
|
||||
@@ -73,7 +73,7 @@ describe('FocusOriginMonitor', () => {
|
||||
|
||||
it('should detect focus via mouse', async(() => {
|
||||
// Simulate focus via mouse.
|
||||
dispatchMousedownEvent(document);
|
||||
dispatchMousedownEvent(buttonElement);
|
||||
buttonElement.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -90,6 +90,25 @@ describe('FocusOriginMonitor', () => {
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should detect focus via touch', async(() => {
|
||||
// Simulate focus via touch.
|
||||
dispatchTouchstartEvent(buttonElement);
|
||||
buttonElement.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
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-touch-focused'))
|
||||
.toBe(true, 'button should have cdk-touch-focused class');
|
||||
expect(changeHandler).toHaveBeenCalledWith('touch');
|
||||
}, TOUCH_BUFFER_MS);
|
||||
}));
|
||||
|
||||
it('should detect programmatic focus', async(() => {
|
||||
// Programmatically focus.
|
||||
buttonElement.focus();
|
||||
@@ -142,6 +161,23 @@ describe('FocusOriginMonitor', () => {
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('focusVia mouse should simulate mouse focus', async(() => {
|
||||
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'touch');
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
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-touch-focused'))
|
||||
.toBe(true, 'button should have cdk-touch-focused class');
|
||||
expect(changeHandler).toHaveBeenCalledWith('touch');
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('focusVia program should simulate programmatic focus', async(() => {
|
||||
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program');
|
||||
fixture.detectChanges();
|
||||
@@ -234,7 +270,7 @@ describe('cdkFocusClasses', () => {
|
||||
|
||||
it('should detect focus via mouse', async(() => {
|
||||
// Simulate focus via mouse.
|
||||
dispatchMousedownEvent(document);
|
||||
dispatchMousedownEvent(buttonElement);
|
||||
buttonElement.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -251,6 +287,25 @@ describe('cdkFocusClasses', () => {
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
it('should detect focus via touch', async(() => {
|
||||
// Simulate focus via touch.
|
||||
dispatchTouchstartEvent(buttonElement);
|
||||
buttonElement.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
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-touch-focused'))
|
||||
.toBe(true, 'button should have cdk-touch-focused class');
|
||||
expect(changeHandler).toHaveBeenCalledWith('touch');
|
||||
}, TOUCH_BUFFER_MS);
|
||||
}));
|
||||
|
||||
it('should detect programmatic focus', async(() => {
|
||||
// Programmatically focus.
|
||||
buttonElement.focus();
|
||||
@@ -312,6 +367,13 @@ function dispatchMousedownEvent(element: Node) {
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/** Dispatches a mousedown event on the specified element. */
|
||||
function dispatchTouchstartEvent(element: Node) {
|
||||
let event = document.createEvent('MouseEvent');
|
||||
event.initMouseEvent(
|
||||
'touchstart', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/** Dispatches a keydown event on the specified element. */
|
||||
function dispatchKeydownEvent(element: Node, keyCode: number) {
|
||||
|
||||
@@ -3,7 +3,12 @@ import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
|
||||
export type FocusOrigin = 'mouse' | 'keyboard' | 'program';
|
||||
// This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
|
||||
// that a value of around 650ms seems appropriate.
|
||||
export const TOUCH_BUFFER_MS = 650;
|
||||
|
||||
|
||||
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program';
|
||||
|
||||
|
||||
/** Monitors mouse and keyboard events to determine the cause of focus events. */
|
||||
@@ -18,14 +23,40 @@ export class FocusOriginMonitor {
|
||||
/** Whether the window has just been focused. */
|
||||
private _windowFocused = false;
|
||||
|
||||
/** The target of the last touch event. */
|
||||
private _lastTouchTarget: EventTarget;
|
||||
|
||||
/** The timeout id of the touch timeout, used to cancel timeout later. */
|
||||
private _touchTimeout: number;
|
||||
|
||||
constructor() {
|
||||
// Listen to keydown and mousedown in the capture phase so we can detect them even if the user
|
||||
// stops propagation.
|
||||
// TODO(mmalerba): Figure out how to handle touchstart
|
||||
document.addEventListener(
|
||||
'keydown', () => this._setOriginForCurrentEventQueue('keyboard'), true);
|
||||
document.addEventListener(
|
||||
'mousedown', () => this._setOriginForCurrentEventQueue('mouse'), true);
|
||||
// Note: we listen to events in the capture phase so we can detect them even if the user stops
|
||||
// propagation.
|
||||
|
||||
// On keydown record the origin and clear any touch event that may be in progress.
|
||||
document.addEventListener('keydown', () => {
|
||||
this._lastTouchTarget = null;
|
||||
this._setOriginForCurrentEventQueue('keyboard');
|
||||
}, true);
|
||||
|
||||
// On mousedown record the origin only if there is not touch target, since a mousedown can
|
||||
// happen as a result of a touch event.
|
||||
document.addEventListener('mousedown', () => {
|
||||
if (!this._lastTouchTarget) {
|
||||
this._setOriginForCurrentEventQueue('mouse');
|
||||
}
|
||||
}, true);
|
||||
|
||||
// When the touchstart event fires the focus event is not yet in the event queue. This means we
|
||||
// can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to see if
|
||||
// a focus happens.
|
||||
document.addEventListener('touchstart', (event: Event) => {
|
||||
if (this._touchTimeout != null) {
|
||||
clearTimeout(this._touchTimeout);
|
||||
}
|
||||
this._lastTouchTarget = event.target;
|
||||
this._touchTimeout = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
||||
}, true);
|
||||
|
||||
// Make a note of when the window regains focus, so we can restore the origin info for the
|
||||
// focused element.
|
||||
@@ -38,7 +69,8 @@ export class FocusOriginMonitor {
|
||||
/** Register an element to receive focus classes. */
|
||||
registerElementForFocusClasses(element: Element, renderer: Renderer): Observable<FocusOrigin> {
|
||||
let subject = new Subject<FocusOrigin>();
|
||||
renderer.listen(element, 'focus', () => this._onFocus(element, renderer, subject));
|
||||
renderer.listen(element, 'focus',
|
||||
(event: Event) => this._onFocus(event, element, renderer, subject));
|
||||
renderer.listen(element, 'blur', () => this._onBlur(element, renderer, subject));
|
||||
return subject.asObservable();
|
||||
}
|
||||
@@ -55,27 +87,56 @@ export class FocusOriginMonitor {
|
||||
setTimeout(() => this._origin = null, 0);
|
||||
}
|
||||
|
||||
/** Checks whether the given focus event was caused by a touchstart event. */
|
||||
private _wasCausedByTouch(event: Event): boolean {
|
||||
// Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
|
||||
// Consider the following dom structure:
|
||||
//
|
||||
// <div #parent tabindex="0" cdkFocusClasses>
|
||||
// <div #child (click)="#parent.focus()"></div>
|
||||
// </div>
|
||||
//
|
||||
// If the user touches the #child element and the #parent is programmatically focused as a
|
||||
// result, this code will still consider it to have been caused by the touch event and will
|
||||
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
|
||||
// relatively small edge-case that can be worked around by using
|
||||
// focusVia(parentEl, renderer, 'program') to focus the parent element.
|
||||
//
|
||||
// If we decide that we absolutely must handle this case correctly, we can do so by listening
|
||||
// for the first focus event after the touchstart, and then the first blur event after that
|
||||
// focus event. When that blur event fires we know that whatever follows is not a result of the
|
||||
// touchstart.
|
||||
let focusTarget = event.target;
|
||||
return this._lastTouchTarget instanceof Node && focusTarget instanceof Node &&
|
||||
(focusTarget == this._lastTouchTarget || focusTarget.contains(this._lastTouchTarget));
|
||||
}
|
||||
|
||||
/** Handles focus events on a registered element. */
|
||||
private _onFocus(element: Element, renderer: Renderer, subject: Subject<FocusOrigin>) {
|
||||
private _onFocus(event: Event, element: Element, renderer: Renderer,
|
||||
subject: Subject<FocusOrigin>) {
|
||||
// If we couldn't detect a cause for the focus event, it's due to one of two reasons:
|
||||
// 1) The window has just regained focus, in which case we want to restore the focused state of
|
||||
// the element from before the window blurred.
|
||||
// 2) The element was programmatically focused, in which case we should mark the origin as
|
||||
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
|
||||
// 3) The element was programmatically focused, in which case we should mark the origin as
|
||||
// 'program'.
|
||||
if (!this._origin) {
|
||||
if (this._windowFocused && this._lastFocusOrigin) {
|
||||
this._origin = this._lastFocusOrigin;
|
||||
} else if (this._wasCausedByTouch(event)) {
|
||||
this._origin = 'touch';
|
||||
} else {
|
||||
this._origin = 'program';
|
||||
}
|
||||
}
|
||||
|
||||
renderer.setElementClass(element, 'cdk-focused', true);
|
||||
renderer.setElementClass(element, 'cdk-touch-focused', this._origin == 'touch');
|
||||
renderer.setElementClass(element, 'cdk-keyboard-focused', this._origin == 'keyboard');
|
||||
renderer.setElementClass(element, 'cdk-mouse-focused', this._origin == 'mouse');
|
||||
renderer.setElementClass(element, 'cdk-program-focused', this._origin == 'program');
|
||||
|
||||
subject.next(this._origin);
|
||||
|
||||
this._lastFocusOrigin = this._origin;
|
||||
this._origin = null;
|
||||
}
|
||||
@@ -83,6 +144,7 @@ export class FocusOriginMonitor {
|
||||
/** Handles blur events on a registered element. */
|
||||
private _onBlur(element: Element, renderer: Renderer, subject: Subject<FocusOrigin>) {
|
||||
renderer.setElementClass(element, 'cdk-focused', false);
|
||||
renderer.setElementClass(element, 'cdk-touch-focused', false);
|
||||
renderer.setElementClass(element, 'cdk-keyboard-focused', false);
|
||||
renderer.setElementClass(element, 'cdk-mouse-focused', false);
|
||||
renderer.setElementClass(element, 'cdk-program-focused', false);
|
||||
|
||||
Reference in New Issue
Block a user