/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { ElementRef, NgZone, Component, ChangeDetectionStrategy, ViewEncapsulation, Injector, } from '@angular/core'; import {Platform, normalizePassiveListenerOptions, _getEventTarget} from '@angular/cdk/platform'; import {isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader} from '@angular/cdk/a11y'; import {coerceElement} from '@angular/cdk/coercion'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {RippleRef, RippleState, RippleConfig} from './ripple-ref'; import {RippleEventManager} from './ripple-event-manager'; /** * Interface that describes the target for launching ripples. * It defines the ripple configuration and disabled state for interaction ripples. * @docs-private */ export interface RippleTarget { /** Configuration for ripples that are launched on pointer down. */ rippleConfig: RippleConfig; /** Whether ripples on pointer down should be disabled. */ rippleDisabled: boolean; } /** Interfaces the defines ripple element transition event listeners. */ interface RippleEventListeners { onTransitionEnd: EventListener; onTransitionCancel: EventListener; fallbackTimer: ReturnType | null; } /** * Default ripple animation configuration for ripples without an explicit * animation config specified. */ export const defaultRippleAnimationConfig = { enterDuration: 225, exitDuration: 150, }; /** * Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch * events to avoid synthetic mouse events. */ const ignoreMouseEventsTimeout = 800; /** Options used to bind a passive capturing event. */ const passiveCapturingEventOptions = normalizePassiveListenerOptions({ passive: true, capture: 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']; @Component({ template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styleUrl: 'ripple-structure.css', host: {'mat-ripple-style-loader': ''}, }) export class _MatRippleStylesLoader {} /** * 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 * event handlers to be installed on the element that triggers ripple animations. * This will eventually become a custom renderer once Angular support exists. * @docs-private */ export class RippleRenderer implements EventListenerObject { /** Element where the ripples are being added to. */ private _containerElement: HTMLElement; /** Element which triggers the ripple elements on mouse events. */ private _triggerElement: HTMLElement | null; /** Whether the pointer is currently down or not. */ private _isPointerDown = false; /** * Map of currently active ripple references. * The ripple reference is mapped to its element event listeners. * The reason why `| null` is used is that event listeners are added only * when the condition is truthy (see the `_startFadeOutTransition` method). */ private _activeRipples = new Map(); /** Latest non-persistent ripple that was triggered. */ private _mostRecentTransientRipple: RippleRef | null; /** 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. */ private _containerRect: DOMRect | null; private static _eventManager = new RippleEventManager(); constructor( private _target: RippleTarget, private _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef, private _platform: Platform, injector?: Injector, ) { // Only do anything if we're on the browser. if (_platform.isBrowser) { this._containerElement = coerceElement(elementOrElementRef); } if (injector) { injector.get(_CdkPrivateStyleLoader).load(_MatRippleStylesLoader); } } /** * Fades in a ripple at the given coordinates. * @param x Coordinate within the element, along the X axis at which to start the ripple. * @param y Coordinate within the element, along the Y axis at which to start the ripple. * @param config Extra ripple options. */ fadeInRipple(x: number, y: number, config: RippleConfig = {}): RippleRef { const containerRect = (this._containerRect = this._containerRect || this._containerElement.getBoundingClientRect()); const animationConfig = {...defaultRippleAnimationConfig, ...config.animation}; if (config.centered) { x = containerRect.left + containerRect.width / 2; y = containerRect.top + containerRect.height / 2; } const radius = config.radius || distanceToFurthestCorner(x, y, containerRect); const offsetX = x - containerRect.left; const offsetY = y - containerRect.top; const enterDuration = animationConfig.enterDuration; const ripple = document.createElement('div'); ripple.classList.add('mat-ripple-element'); ripple.style.left = `${offsetX - radius}px`; ripple.style.top = `${offsetY - radius}px`; ripple.style.height = `${radius * 2}px`; ripple.style.width = `${radius * 2}px`; // If a custom color has been specified, set it as inline style. If no color is // set, the default color will be applied through the ripple theme styles. if (config.color != null) { ripple.style.backgroundColor = config.color; } ripple.style.transitionDuration = `${enterDuration}ms`; this._containerElement.appendChild(ripple); // By default the browser does not recalculate the styles of dynamically created // ripple elements. This is critical to ensure that the `scale` animates properly. // We enforce a style recalculation by calling `getComputedStyle` and *accessing* a property. // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a const computedStyles = window.getComputedStyle(ripple); const userTransitionProperty = computedStyles.transitionProperty; const userTransitionDuration = computedStyles.transitionDuration; // Note: We detect whether animation is forcibly disabled through CSS (e.g. through // `transition: none` or `display: none`). This is technically unexpected since animations are // controlled through the animation config, but this exists for backwards compatibility. This // logic does not need to be super accurate since it covers some edge cases which can be easily // avoided by users. const animationForciblyDisabledThroughCss = userTransitionProperty === 'none' || // Note: The canonical unit for serialized CSS `