From 692e17028c41d2233e5c4eda924fc8b01651bc5d Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 25 Sep 2017 17:38:58 +0200 Subject: [PATCH 1/2] 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 --- src/cdk/testing/dispatch-events.ts | 12 +++++++- src/cdk/testing/event-objects.ts | 20 ++++++++++++- src/lib/core/ripple/ripple-renderer.ts | 41 +++++++++++++++++--------- src/lib/core/ripple/ripple.spec.ts | 16 +++++++++- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/cdk/testing/dispatch-events.ts b/src/cdk/testing/dispatch-events.ts index f815fce1a4b9..9163c33db07f 100644 --- a/src/cdk/testing/dispatch-events.ts +++ b/src/cdk/testing/dispatch-events.ts @@ -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 { @@ -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)); +} diff --git a/src/cdk/testing/event-objects.ts b/src/cdk/testing/event-objects.ts index 856aad2f09f4..f8000e1ec9ff 100644 --- a/src/cdk/testing/event-objects.ts +++ b/src/cdk/testing/event-objects.ts @@ -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 */ @@ -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; diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index b6875b691ac3..1ab8013123ee 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -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(); @@ -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); @@ -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); @@ -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 => { @@ -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); } } diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 5fb8608a8fe9..f4b56cce6a25 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -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 { MdRipple, MdRippleModule, MD_RIPPLE_GLOBAL_OPTIONS, RippleState, RippleGlobalOptions @@ -107,6 +107,20 @@ describe('MdRipple', () => { 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'); From f1eedc05728c55747000a080024ce9103bdc3788 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 26 Sep 2017 07:53:20 +0200 Subject: [PATCH 2/2] Address feedback --- src/cdk/testing/event-objects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk/testing/event-objects.ts b/src/cdk/testing/event-objects.ts index f8000e1ec9ff..7b2fe2407bc4 100644 --- a/src/cdk/testing/event-objects.ts +++ b/src/cdk/testing/event-objects.ts @@ -41,7 +41,7 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 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] } + touches: {value: [touchDetails]} }); return event;