diff --git a/src/cdk/testing/event-objects.ts b/src/cdk/testing/event-objects.ts index 96d57d0664d5..e02fbabdbf8b 100644 --- a/src/cdk/testing/event-objects.ts +++ b/src/cdk/testing/event-objects.ts @@ -42,7 +42,8 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0) { // the touch details. Object.defineProperties(event, { touches: {value: [touchDetails]}, - targetTouches: {value: [touchDetails]} + targetTouches: {value: [touchDetails]}, + changedTouches: {value: [touchDetails]} }); return event; diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index bb29ddd85984..24edac673fee 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -264,8 +264,13 @@ export class RippleRenderer { this._lastTouchStartEvent = Date.now(); this._isPointerDown = true; - this.fadeInRipple( - event.touches[0].clientX, event.touches[0].clientY, this._target.rippleConfig); + // Use `changedTouches` so we skip any touches where the user put + // their finger down, but used another finger to tap the element again. + const touches = event.changedTouches; + + for (let i = 0; i < touches.length; i++) { + this.fadeInRipple(touches[i].clientX, touches[i].clientY, this._target.rippleConfig); + } } } diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index c4b6f2c93a3a..29033e0610eb 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -1,7 +1,12 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; import {Platform} from '@angular/cdk/platform'; -import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing'; +import { + dispatchEvent, + createTouchEvent, + dispatchMouseEvent, + dispatchTouchEvent, +} from '@angular/cdk/testing'; import {defaultRippleAnimationConfig, RippleAnimationConfig} from './ripple-renderer'; import { MatRipple, MatRippleModule, MAT_RIPPLE_GLOBAL_OPTIONS, RippleState, RippleGlobalOptions @@ -121,6 +126,32 @@ describe('MatRipple', () => { expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); })); + it('should launch multiple ripples for multi-touch', fakeAsync(() => { + const touchEvent = createTouchEvent('touchstart'); + + Object.defineProperties(touchEvent, { + changedTouches: { + value: [ + {pageX: 0, pageY: 0}, + {pageX: 10, pageY: 10}, + {pageX: 20, pageY: 20} + ] + } + }); + + dispatchEvent(rippleTarget, touchEvent); + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(3); + + tick(enterDuration); + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(3); + + dispatchTouchEvent(rippleTarget, 'touchend'); + + tick(exitDuration); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + })); + it('should ignore synthetic mouse events after touchstart', () => fakeAsync(() => { dispatchTouchEvent(rippleTarget, 'touchstart'); dispatchTouchEvent(rippleTarget, 'mousedown'); diff --git a/src/material-examples/ripple-overview/ripple-overview-example.css b/src/material-examples/ripple-overview/ripple-overview-example.css index 7c5b887ee3c2..4c767bbd19be 100644 --- a/src/material-examples/ripple-overview/ripple-overview-example.css +++ b/src/material-examples/ripple-overview/ripple-overview-example.css @@ -5,6 +5,8 @@ width: 300px; height: 300px; line-height: 300px; + -webkit-user-drag: none; + -webkit-tap-highlight-color: transparent; } /** Styles to make the demo look better. */