diff --git a/src/material/core/ripple/ripple-renderer.ts b/src/material/core/ripple/ripple-renderer.ts index c3e2629b3cae..2be3a2a9e511 100644 --- a/src/material/core/ripple/ripple-renderer.ts +++ b/src/material/core/ripple/ripple-renderer.ts @@ -61,6 +61,12 @@ const ignoreMouseEventsTimeout = 800; /** Options that apply to all the event listeners that are bound by the ripple renderer. */ const passiveEventOptions = normalizePassiveListenerOptions({passive: true}); +/** Events that signal that the pointer is down. */ +const pointerDownEvents = ['mousedown', 'touchstart']; + +/** Events that signal that the pointer is up. */ +const pointerUpEvents = ['mouseup', 'mouseleave', 'touchend', 'touchcancel']; + /** * Helper service that performs DOM manipulations. Not intended to be used outside this module. * The constructor takes a reference to the ripple directive's host element and a map of DOM @@ -68,7 +74,7 @@ const passiveEventOptions = normalizePassiveListenerOptions({passive: true}); * This will eventually become a custom renderer once Angular support exists. * @docs-private */ -export class RippleRenderer { +export class RippleRenderer implements EventListenerObject { /** Element where the ripples are being added to. */ private _containerElement: HTMLElement; @@ -78,9 +84,6 @@ export class RippleRenderer { /** Whether the pointer is currently down or not. */ private _isPointerDown = false; - /** Events to be registered on the trigger element. */ - private _triggerEvents = new Map(); - /** Set of currently active ripple references. */ private _activeRipples = new Set(); @@ -90,6 +93,9 @@ export class RippleRenderer { /** Time in milliseconds when the last touchstart event happened. */ private _lastTouchStartEvent: number; + /** Whether pointer-up event listeners have been registered. */ + private _pointerUpEventsRegistered = false; + /** * Cached dimensions of the ripple container. Set when the first * ripple is shown and cleared once no more ripples are visible. @@ -104,16 +110,6 @@ export class RippleRenderer { // Only do anything if we're on the browser. if (platform.isBrowser) { this._containerElement = coerceElement(elementOrElementRef); - - // Specify events which need to be registered on the trigger. - this._triggerEvents - .set('mousedown', this._onMousedown) - .set('mouseup', this._onPointerUp) - .set('mouseleave', this._onPointerUp) - - .set('touchstart', this._onTouchStart) - .set('touchend', this._onPointerUp) - .set('touchcancel', this._onPointerUp); } } @@ -241,17 +237,34 @@ export class RippleRenderer { // Remove all previously registered event listeners from the trigger element. this._removeTriggerEvents(); - this._ngZone.runOutsideAngular(() => { - this._triggerEvents.forEach((fn, type) => { - element.addEventListener(type, fn, passiveEventOptions); - }); - }); - this._triggerElement = element; + this._registerEvents(pointerDownEvents); + } + + /** + * Handles all registered events. + * @docs-private + */ + handleEvent(event: Event) { + if (event.type === 'mousedown') { + this._onMousedown(event as MouseEvent); + } else if (event.type === 'touchstart') { + this._onTouchStart(event as TouchEvent); + } else { + this._onPointerUp(); + } + + // If pointer-up events haven't been registered yet, do so now. + // We do this on-demand in order to reduce the total number of event listeners + // registered by the ripples, which speeds up the rendering time for large UIs. + if (!this._pointerUpEventsRegistered) { + this._registerEvents(pointerUpEvents); + this._pointerUpEventsRegistered = true; + } } /** Function being called whenever the trigger is being pressed using mouse. */ - private _onMousedown = (event: MouseEvent) => { + private _onMousedown(event: MouseEvent) { // Screen readers will fire fake mouse events for space/enter. Skip launching a // ripple in this case for consistency with the non-screen-reader experience. const isFakeMousedown = isFakeMousedownFromScreenReader(event); @@ -265,7 +278,7 @@ export class RippleRenderer { } /** Function being called whenever the trigger is being pressed using touch. */ - private _onTouchStart = (event: TouchEvent) => { + private _onTouchStart(event: TouchEvent) { if (!this._target.rippleDisabled) { // Some browsers fire mouse events after a `touchstart` event. Those synthetic mouse // events will launch a second ripple if we don't ignore mouse events for a specific @@ -284,7 +297,7 @@ export class RippleRenderer { } /** Function being called whenever the trigger is being released. */ - private _onPointerUp = () => { + private _onPointerUp() { if (!this._isPointerDown) { return; } @@ -309,12 +322,27 @@ export class RippleRenderer { this._ngZone.runOutsideAngular(() => setTimeout(fn, delay)); } + /** Registers event listeners for a given list of events. */ + private _registerEvents(eventTypes: string[]) { + this._ngZone.runOutsideAngular(() => { + eventTypes.forEach((type) => { + this._triggerElement!.addEventListener(type, this, passiveEventOptions); + }); + }); + } + /** Removes previously registered event listeners from the trigger element. */ _removeTriggerEvents() { if (this._triggerElement) { - this._triggerEvents.forEach((fn, type) => { - this._triggerElement!.removeEventListener(type, fn, passiveEventOptions); + pointerDownEvents.forEach((type) => { + this._triggerElement!.removeEventListener(type, this, passiveEventOptions); }); + + if (this._pointerUpEventsRegistered) { + pointerUpEvents.forEach((type) => { + this._triggerElement!.removeEventListener(type, this, passiveEventOptions); + }); + } } } } diff --git a/tools/public_api_guard/material/core.d.ts b/tools/public_api_guard/material/core.d.ts index a100dfe103c9..3df5ded1c9d6 100644 --- a/tools/public_api_guard/material/core.d.ts +++ b/tools/public_api_guard/material/core.d.ts @@ -434,12 +434,13 @@ export declare class RippleRef { fadeOut(): void; } -export declare class RippleRenderer { +export declare class RippleRenderer implements EventListenerObject { constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef, platform: Platform); _removeTriggerEvents(): void; fadeInRipple(x: number, y: number, config?: RippleConfig): RippleRef; fadeOutAll(): void; fadeOutRipple(rippleRef: RippleRef): void; + handleEvent(event: Event): void; setupTriggerEvents(elementOrElementRef: HTMLElement | ElementRef): void; }