494 lines
19 KiB
TypeScript
494 lines
19 KiB
TypeScript
|
|
/**
|
||
|
|
* @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<typeof setTimeout> | 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<RippleRef, RippleEventListeners | null>();
|
||
|
|
|
||
|
|
/** 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<HTMLElement>,
|
||
|
|
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 `<time>` properties is seconds. Additionally
|
||
|
|
// some browsers expand the duration for every property (in our case `opacity` and `transform`).
|
||
|
|
userTransitionDuration === '0s' ||
|
||
|
|
userTransitionDuration === '0s, 0s' ||
|
||
|
|
// If the container is 0x0, it's likely `display: none`.
|
||
|
|
(containerRect.width === 0 && containerRect.height === 0);
|
||
|
|
|
||
|
|
// Exposed reference to the ripple that will be returned.
|
||
|
|
const rippleRef = new RippleRef(this, ripple, config, animationForciblyDisabledThroughCss);
|
||
|
|
|
||
|
|
// Start the enter animation by setting the transform/scale to 100%. The animation will
|
||
|
|
// execute as part of this statement because we forced a style recalculation before.
|
||
|
|
// Note: We use a 3d transform here in order to avoid an issue in Safari where
|
||
|
|
// the ripples aren't clipped when inside the shadow DOM (see #24028).
|
||
|
|
ripple.style.transform = 'scale3d(1, 1, 1)';
|
||
|
|
|
||
|
|
rippleRef.state = RippleState.FADING_IN;
|
||
|
|
|
||
|
|
if (!config.persistent) {
|
||
|
|
this._mostRecentTransientRipple = rippleRef;
|
||
|
|
}
|
||
|
|
|
||
|
|
let eventListeners: RippleEventListeners | null = null;
|
||
|
|
|
||
|
|
// Do not register the `transition` event listener if fade-in and fade-out duration
|
||
|
|
// are set to zero. The events won't fire anyway and we can save resources here.
|
||
|
|
if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) {
|
||
|
|
this._ngZone.runOutsideAngular(() => {
|
||
|
|
const onTransitionEnd = () => {
|
||
|
|
// Clear the fallback timer since the transition fired correctly.
|
||
|
|
if (eventListeners) {
|
||
|
|
eventListeners.fallbackTimer = null;
|
||
|
|
}
|
||
|
|
clearTimeout(fallbackTimer);
|
||
|
|
this._finishRippleTransition(rippleRef);
|
||
|
|
};
|
||
|
|
const onTransitionCancel = () => this._destroyRipple(rippleRef);
|
||
|
|
|
||
|
|
// In some cases where there's a higher load on the browser, it can choose not to dispatch
|
||
|
|
// neither `transitionend` nor `transitioncancel` (see b/227356674). This timer serves as a
|
||
|
|
// fallback for such cases so that the ripple doesn't become stuck. We add a 100ms buffer
|
||
|
|
// because timers aren't precise. Note that another approach can be to transition the ripple
|
||
|
|
// to the `VISIBLE` state immediately above and to `FADING_IN` afterwards inside
|
||
|
|
// `transitionstart`. We go with the timer because it's one less event listener and
|
||
|
|
// it's less likely to break existing tests.
|
||
|
|
const fallbackTimer = setTimeout(onTransitionCancel, enterDuration + 100);
|
||
|
|
|
||
|
|
ripple.addEventListener('transitionend', onTransitionEnd);
|
||
|
|
// If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple
|
||
|
|
// directly as otherwise we would keep it part of the ripple container forever.
|
||
|
|
// https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document.
|
||
|
|
ripple.addEventListener('transitioncancel', onTransitionCancel);
|
||
|
|
eventListeners = {onTransitionEnd, onTransitionCancel, fallbackTimer};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the ripple reference to the list of all active ripples.
|
||
|
|
this._activeRipples.set(rippleRef, eventListeners);
|
||
|
|
|
||
|
|
// In case there is no fade-in transition duration, we need to manually call the transition
|
||
|
|
// end listener because `transitionend` doesn't fire if there is no transition.
|
||
|
|
if (animationForciblyDisabledThroughCss || !enterDuration) {
|
||
|
|
this._finishRippleTransition(rippleRef);
|
||
|
|
}
|
||
|
|
|
||
|
|
return rippleRef;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Fades out a ripple reference. */
|
||
|
|
fadeOutRipple(rippleRef: RippleRef) {
|
||
|
|
// For ripples already fading out or hidden, this should be a noop.
|
||
|
|
if (rippleRef.state === RippleState.FADING_OUT || rippleRef.state === RippleState.HIDDEN) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rippleEl = rippleRef.element;
|
||
|
|
const animationConfig = {...defaultRippleAnimationConfig, ...rippleRef.config.animation};
|
||
|
|
|
||
|
|
// This starts the fade-out transition and will fire the transition end listener that
|
||
|
|
// removes the ripple element from the DOM.
|
||
|
|
rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
|
||
|
|
rippleEl.style.opacity = '0';
|
||
|
|
rippleRef.state = RippleState.FADING_OUT;
|
||
|
|
|
||
|
|
// In case there is no fade-out transition duration, we need to manually call the
|
||
|
|
// transition end listener because `transitionend` doesn't fire if there is no transition.
|
||
|
|
if (rippleRef._animationForciblyDisabledThroughCss || !animationConfig.exitDuration) {
|
||
|
|
this._finishRippleTransition(rippleRef);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Fades out all currently active ripples. */
|
||
|
|
fadeOutAll() {
|
||
|
|
this._getActiveRipples().forEach(ripple => ripple.fadeOut());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Fades out all currently active non-persistent ripples. */
|
||
|
|
fadeOutAllNonPersistent() {
|
||
|
|
this._getActiveRipples().forEach(ripple => {
|
||
|
|
if (!ripple.config.persistent) {
|
||
|
|
ripple.fadeOut();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Sets up the trigger event listeners */
|
||
|
|
setupTriggerEvents(elementOrElementRef: HTMLElement | ElementRef<HTMLElement>) {
|
||
|
|
const element = coerceElement(elementOrElementRef);
|
||
|
|
|
||
|
|
if (!this._platform.isBrowser || !element || element === this._triggerElement) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove all previously registered event listeners from the trigger element.
|
||
|
|
this._removeTriggerEvents();
|
||
|
|
this._triggerElement = element;
|
||
|
|
|
||
|
|
// Use event delegation for the trigger events since they're
|
||
|
|
// set up during creation and are performance-sensitive.
|
||
|
|
pointerDownEvents.forEach(type => {
|
||
|
|
RippleRenderer._eventManager.addHandler(this._ngZone, type, element, this);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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) {
|
||
|
|
// The events for hiding the ripple are bound directly on the trigger, because:
|
||
|
|
// 1. Some of them occur frequently (e.g. `mouseleave`) and any advantage we get from
|
||
|
|
// delegation will be diminished by having to look through all the data structures often.
|
||
|
|
// 2. They aren't as performance-sensitive, because they're bound only after the user
|
||
|
|
// has interacted with an element.
|
||
|
|
this._ngZone.runOutsideAngular(() => {
|
||
|
|
pointerUpEvents.forEach(type => {
|
||
|
|
this._triggerElement!.addEventListener(type, this, passiveCapturingEventOptions);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
this._pointerUpEventsRegistered = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Method that will be called if the fade-in or fade-in transition completed. */
|
||
|
|
private _finishRippleTransition(rippleRef: RippleRef) {
|
||
|
|
if (rippleRef.state === RippleState.FADING_IN) {
|
||
|
|
this._startFadeOutTransition(rippleRef);
|
||
|
|
} else if (rippleRef.state === RippleState.FADING_OUT) {
|
||
|
|
this._destroyRipple(rippleRef);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Starts the fade-out transition of the given ripple if it's not persistent and the pointer
|
||
|
|
* is not held down anymore.
|
||
|
|
*/
|
||
|
|
private _startFadeOutTransition(rippleRef: RippleRef) {
|
||
|
|
const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
|
||
|
|
const {persistent} = rippleRef.config;
|
||
|
|
|
||
|
|
rippleRef.state = RippleState.VISIBLE;
|
||
|
|
|
||
|
|
// When the timer runs out while the user has kept their pointer down, we want to
|
||
|
|
// keep only the persistent ripples and the latest transient ripple. We do this,
|
||
|
|
// because we don't want stacked transient ripples to appear after their enter
|
||
|
|
// animation has finished.
|
||
|
|
if (!persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
|
||
|
|
rippleRef.fadeOut();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Destroys the given ripple by removing it from the DOM and updating its state. */
|
||
|
|
private _destroyRipple(rippleRef: RippleRef) {
|
||
|
|
const eventListeners = this._activeRipples.get(rippleRef) ?? null;
|
||
|
|
this._activeRipples.delete(rippleRef);
|
||
|
|
|
||
|
|
// Clear out the cached bounding rect if we have no more ripples.
|
||
|
|
if (!this._activeRipples.size) {
|
||
|
|
this._containerRect = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the current ref is the most recent transient ripple, unset it
|
||
|
|
// avoid memory leaks.
|
||
|
|
if (rippleRef === this._mostRecentTransientRipple) {
|
||
|
|
this._mostRecentTransientRipple = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
rippleRef.state = RippleState.HIDDEN;
|
||
|
|
if (eventListeners !== null) {
|
||
|
|
rippleRef.element.removeEventListener('transitionend', eventListeners.onTransitionEnd);
|
||
|
|
rippleRef.element.removeEventListener('transitioncancel', eventListeners.onTransitionCancel);
|
||
|
|
if (eventListeners.fallbackTimer !== null) {
|
||
|
|
clearTimeout(eventListeners.fallbackTimer);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rippleRef.element.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Function being called whenever the trigger is being pressed using mouse. */
|
||
|
|
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);
|
||
|
|
const isSyntheticEvent =
|
||
|
|
this._lastTouchStartEvent &&
|
||
|
|
Date.now() < this._lastTouchStartEvent + ignoreMouseEventsTimeout;
|
||
|
|
|
||
|
|
if (!this._target.rippleDisabled && !isFakeMousedown && !isSyntheticEvent) {
|
||
|
|
this._isPointerDown = true;
|
||
|
|
this.fadeInRipple(event.clientX, event.clientY, this._target.rippleConfig);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Function being called whenever the trigger is being pressed using touch. */
|
||
|
|
private _onTouchStart(event: TouchEvent) {
|
||
|
|
if (!this._target.rippleDisabled && !isFakeTouchstartFromScreenReader(event)) {
|
||
|
|
// 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
|
||
|
|
// time after a touchstart event.
|
||
|
|
this._lastTouchStartEvent = Date.now();
|
||
|
|
this._isPointerDown = true;
|
||
|
|
|
||
|
|
// 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 as TouchList | undefined;
|
||
|
|
|
||
|
|
// According to the typings the touches should always be defined, but in some cases
|
||
|
|
// the browser appears to not assign them in tests which leads to flakes.
|
||
|
|
if (touches) {
|
||
|
|
for (let i = 0; i < touches.length; i++) {
|
||
|
|
this.fadeInRipple(touches[i].clientX, touches[i].clientY, this._target.rippleConfig);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Function being called whenever the trigger is being released. */
|
||
|
|
private _onPointerUp() {
|
||
|
|
if (!this._isPointerDown) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._isPointerDown = false;
|
||
|
|
|
||
|
|
// Fade-out all ripples that are visible and not persistent.
|
||
|
|
this._getActiveRipples().forEach(ripple => {
|
||
|
|
// By default, only ripples that are completely visible will fade out on pointer release.
|
||
|
|
// If the `terminateOnPointerUp` option is set, ripples that still fade in will also fade out.
|
||
|
|
const isVisible =
|
||
|
|
ripple.state === RippleState.VISIBLE ||
|
||
|
|
(ripple.config.terminateOnPointerUp && ripple.state === RippleState.FADING_IN);
|
||
|
|
|
||
|
|
if (!ripple.config.persistent && isVisible) {
|
||
|
|
ripple.fadeOut();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getActiveRipples(): RippleRef[] {
|
||
|
|
return Array.from(this._activeRipples.keys());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Removes previously registered event listeners from the trigger element. */
|
||
|
|
_removeTriggerEvents() {
|
||
|
|
const trigger = this._triggerElement;
|
||
|
|
|
||
|
|
if (trigger) {
|
||
|
|
pointerDownEvents.forEach(type =>
|
||
|
|
RippleRenderer._eventManager.removeHandler(type, trigger, this),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (this._pointerUpEventsRegistered) {
|
||
|
|
pointerUpEvents.forEach(type =>
|
||
|
|
trigger.removeEventListener(type, this, passiveCapturingEventOptions),
|
||
|
|
);
|
||
|
|
|
||
|
|
this._pointerUpEventsRegistered = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
|
||
|
|
*/
|
||
|
|
function distanceToFurthestCorner(x: number, y: number, rect: DOMRect) {
|
||
|
|
const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right));
|
||
|
|
const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom));
|
||
|
|
return Math.sqrt(distX * distX + distY * distY);
|
||
|
|
}
|