Skip to content

Commit

Permalink
fix(ripple): handle touch events (#7299)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
devversion authored and andrewseguin committed Sep 29, 2017
1 parent 92a868e commit fe0864b
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 17 deletions.
12 changes: 11 additions & 1 deletion src/cdk/testing/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import {createFakeEvent, createKeyboardEvent, createMouseEvent} from './event-objects';
import {
createFakeEvent,
createKeyboardEvent,
createMouseEvent,
createTouchEvent
} from './event-objects';

/** Utility to dispatch any event on a Node. */
export function dispatchEvent(node: Node | Window, event: Event): Event {
Expand All @@ -29,3 +34,8 @@ export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0,
event = createMouseEvent(type, x, y)): MouseEvent {
return dispatchEvent(node, event) as MouseEvent;
}

/** Shorthand to dispatch a touch event on the specified coordinates. */
export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) {
return dispatchEvent(node, createTouchEvent(type, x, y));
}
20 changes: 19 additions & 1 deletion src/cdk/testing/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

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

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

/** Creates a browser TouchEvent with the specified pointer coordinates. */
export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
// In favor of creating events that work for most of the browsers, the event is created
// as a basic UI Event. The necessary details for the event will be set manually.
const event = document.createEvent('UIEvent');
const touchDetails = {pageX, pageY};

event.initUIEvent(type, true, true, window, 0);

// Most of the browsers don't have a "initTouchEvent" method that can be used to define
// the touch details.
Object.defineProperties(event, {
touches: {value: [touchDetails]}
});

return event;
}

/** Dispatches a keydown event from an element. */
export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) {
let event = document.createEvent('KeyboardEvent') as any;
Expand Down
41 changes: 27 additions & 14 deletions src/lib/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export class RippleRenderer {
/** Element which triggers the ripple elements on mouse events. */
private _triggerElement: HTMLElement | null;

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

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

// Specify events which need to be registered on the trigger.
this._triggerEvents.set('mousedown', this.onMousedown.bind(this));
this._triggerEvents.set('mouseup', this.onMouseup.bind(this));
this._triggerEvents.set('mouseleave', this.onMouseLeave.bind(this));
this._triggerEvents.set('touchstart', this.onTouchstart.bind(this));

this._triggerEvents.set('mouseup', this.onPointerUp.bind(this));
this._triggerEvents.set('touchend', this.onPointerUp.bind(this));

this._triggerEvents.set('mouseleave', this.onPointerLeave.bind(this));

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

if (!config.persistent && !this._isMousedown) {
if (!config.persistent && !this._isPointerDown) {
rippleRef.fadeOut();
}
}, duration);
Expand Down Expand Up @@ -181,17 +185,17 @@ export class RippleRenderer {
this._triggerElement = element;
}

/** Listener being called on mousedown event. */
/** Function being called whenever the trigger is being pressed. */
private onMousedown(event: MouseEvent) {
if (!this.rippleDisabled) {
this._isMousedown = true;
this._isPointerDown = true;
this.fadeInRipple(event.pageX, event.pageY, this.rippleConfig);
}
}

/** Listener being called on mouseup event. */
private onMouseup() {
this._isMousedown = false;
/** Function being called whenever the pointer is being released. */
private onPointerUp() {
this._isPointerDown = false;

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

/** Listener being called on mouseleave event. */
private onMouseLeave() {
if (this._isMousedown) {
this.onMouseup();
/** Function being called whenever the pointer leaves the trigger. */
private onPointerLeave() {
if (this._isPointerDown) {
this.onPointerUp();
}
}

/** Function being called whenever the trigger is being touched. */
private onTouchstart(event: TouchEvent) {
if (!this.rippleDisabled) {
const {pageX, pageY} = event.touches[0];
this._isPointerDown = true;
this.fadeInRipple(pageX, pageY, this.rippleConfig);
}
}

Expand Down
16 changes: 15 additions & 1 deletion src/lib/core/ripple/ripple.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/
import {Component, ViewChild} from '@angular/core';
import {Platform} from '@angular/cdk/platform';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {dispatchMouseEvent} from '@angular/cdk/testing';
import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing';
import {RIPPLE_FADE_OUT_DURATION, RIPPLE_FADE_IN_DURATION} from './ripple-renderer';
import {
MatRipple, MatRippleModule, MAT_RIPPLE_GLOBAL_OPTIONS, RippleState, RippleGlobalOptions
Expand Down Expand Up @@ -107,6 +107,20 @@ describe('MatRipple', () => {
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(2);
});

it('should launch ripples on touchstart', fakeAsync(() => {
dispatchTouchEvent(rippleTarget, 'touchstart');
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);

tick(RIPPLE_FADE_IN_DURATION);
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);

dispatchTouchEvent(rippleTarget, 'touchend');

tick(RIPPLE_FADE_OUT_DURATION);

expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
}));

it('removes ripple after timeout', fakeAsync(() => {
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
Expand Down

0 comments on commit fe0864b

Please sign in to comment.