Skip to content

Commit fe0864b

Browse files
devversionandrewseguin
authored andcommitted
fix(ripple): handle touch events (#7299)
* fix(ripple): handle touch events * Ripples are now launched on touchstart for touch devices. Before they were just launched after the click happened. Fixes #7062 * Address feedback
1 parent 92a868e commit fe0864b

File tree

4 files changed

+72
-17
lines changed

4 files changed

+72
-17
lines changed

src/cdk/testing/dispatch-events.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {createFakeEvent, createKeyboardEvent, createMouseEvent} from './event-objects';
9+
import {
10+
createFakeEvent,
11+
createKeyboardEvent,
12+
createMouseEvent,
13+
createTouchEvent
14+
} from './event-objects';
1015

1116
/** Utility to dispatch any event on a Node. */
1217
export function dispatchEvent(node: Node | Window, event: Event): Event {
@@ -29,3 +34,8 @@ export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0,
2934
event = createMouseEvent(type, x, y)): MouseEvent {
3035
return dispatchEvent(node, event) as MouseEvent;
3136
}
37+
38+
/** Shorthand to dispatch a touch event on the specified coordinates. */
39+
export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) {
40+
return dispatchEvent(node, createTouchEvent(type, x, y));
41+
}

src/cdk/testing/event-objects.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/** Creates a browser MouseEvent with the specified options. */
1010
export function createMouseEvent(type: string, x = 0, y = 0) {
11-
let event = document.createEvent('MouseEvent');
11+
const event = document.createEvent('MouseEvent');
1212

1313
event.initMouseEvent(type,
1414
false, /* canBubble */
@@ -29,6 +29,24 @@ export function createMouseEvent(type: string, x = 0, y = 0) {
2929
return event;
3030
}
3131

32+
/** Creates a browser TouchEvent with the specified pointer coordinates. */
33+
export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
34+
// In favor of creating events that work for most of the browsers, the event is created
35+
// as a basic UI Event. The necessary details for the event will be set manually.
36+
const event = document.createEvent('UIEvent');
37+
const touchDetails = {pageX, pageY};
38+
39+
event.initUIEvent(type, true, true, window, 0);
40+
41+
// Most of the browsers don't have a "initTouchEvent" method that can be used to define
42+
// the touch details.
43+
Object.defineProperties(event, {
44+
touches: {value: [touchDetails]}
45+
});
46+
47+
return event;
48+
}
49+
3250
/** Dispatches a keydown event from an element. */
3351
export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) {
3452
let event = document.createEvent('KeyboardEvent') as any;

src/lib/core/ripple/ripple-renderer.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export class RippleRenderer {
4141
/** Element which triggers the ripple elements on mouse events. */
4242
private _triggerElement: HTMLElement | null;
4343

44-
/** Whether the mouse is currently down or not. */
45-
private _isMousedown: boolean = false;
44+
/** Whether the pointer is currently being held on the trigger or not. */
45+
private _isPointerDown: boolean = false;
4646

4747
/** Events to be registered on the trigger element. */
4848
private _triggerEvents = new Map<string, any>();
@@ -67,8 +67,12 @@ export class RippleRenderer {
6767

6868
// Specify events which need to be registered on the trigger.
6969
this._triggerEvents.set('mousedown', this.onMousedown.bind(this));
70-
this._triggerEvents.set('mouseup', this.onMouseup.bind(this));
71-
this._triggerEvents.set('mouseleave', this.onMouseLeave.bind(this));
70+
this._triggerEvents.set('touchstart', this.onTouchstart.bind(this));
71+
72+
this._triggerEvents.set('mouseup', this.onPointerUp.bind(this));
73+
this._triggerEvents.set('touchend', this.onPointerUp.bind(this));
74+
75+
this._triggerEvents.set('mouseleave', this.onPointerLeave.bind(this));
7276

7377
// By default use the host element as trigger element.
7478
this.setTriggerElement(this._containerElement);
@@ -128,7 +132,7 @@ export class RippleRenderer {
128132
this.runTimeoutOutsideZone(() => {
129133
rippleRef.state = RippleState.VISIBLE;
130134

131-
if (!config.persistent && !this._isMousedown) {
135+
if (!config.persistent && !this._isPointerDown) {
132136
rippleRef.fadeOut();
133137
}
134138
}, duration);
@@ -181,17 +185,17 @@ export class RippleRenderer {
181185
this._triggerElement = element;
182186
}
183187

184-
/** Listener being called on mousedown event. */
188+
/** Function being called whenever the trigger is being pressed. */
185189
private onMousedown(event: MouseEvent) {
186190
if (!this.rippleDisabled) {
187-
this._isMousedown = true;
191+
this._isPointerDown = true;
188192
this.fadeInRipple(event.pageX, event.pageY, this.rippleConfig);
189193
}
190194
}
191195

192-
/** Listener being called on mouseup event. */
193-
private onMouseup() {
194-
this._isMousedown = false;
196+
/** Function being called whenever the pointer is being released. */
197+
private onPointerUp() {
198+
this._isPointerDown = false;
195199

196200
// Fade-out all ripples that are completely visible and not persistent.
197201
this._activeRipples.forEach(ripple => {
@@ -201,10 +205,19 @@ export class RippleRenderer {
201205
});
202206
}
203207

204-
/** Listener being called on mouseleave event. */
205-
private onMouseLeave() {
206-
if (this._isMousedown) {
207-
this.onMouseup();
208+
/** Function being called whenever the pointer leaves the trigger. */
209+
private onPointerLeave() {
210+
if (this._isPointerDown) {
211+
this.onPointerUp();
212+
}
213+
}
214+
215+
/** Function being called whenever the trigger is being touched. */
216+
private onTouchstart(event: TouchEvent) {
217+
if (!this.rippleDisabled) {
218+
const {pageX, pageY} = event.touches[0];
219+
this._isPointerDown = true;
220+
this.fadeInRipple(pageX, pageY, this.rippleConfig);
208221
}
209222
}
210223

src/lib/core/ripple/ripple.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/
22
import {Component, ViewChild} from '@angular/core';
33
import {Platform} from '@angular/cdk/platform';
44
import {ViewportRuler} from '@angular/cdk/scrolling';
5-
import {dispatchMouseEvent} from '@angular/cdk/testing';
5+
import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing';
66
import {RIPPLE_FADE_OUT_DURATION, RIPPLE_FADE_IN_DURATION} from './ripple-renderer';
77
import {
88
MatRipple, MatRippleModule, MAT_RIPPLE_GLOBAL_OPTIONS, RippleState, RippleGlobalOptions
@@ -107,6 +107,20 @@ describe('MatRipple', () => {
107107
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(2);
108108
});
109109

110+
it('should launch ripples on touchstart', fakeAsync(() => {
111+
dispatchTouchEvent(rippleTarget, 'touchstart');
112+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
113+
114+
tick(RIPPLE_FADE_IN_DURATION);
115+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
116+
117+
dispatchTouchEvent(rippleTarget, 'touchend');
118+
119+
tick(RIPPLE_FADE_OUT_DURATION);
120+
121+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
122+
}));
123+
110124
it('removes ripple after timeout', fakeAsync(() => {
111125
dispatchMouseEvent(rippleTarget, 'mousedown');
112126
dispatchMouseEvent(rippleTarget, 'mouseup');

0 commit comments

Comments
 (0)