UNPKG

@material/ripple

Version:

The Material Components for the web Ink Ripple effect for web element interactions

524 lines • 26 kB
/** * @license * Copyright 2016 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import { __assign, __extends, __values } from "tslib"; import { MDCFoundation } from '@material/base/foundation'; import { cssClasses, numbers, strings } from './constants'; import { getNormalizedEventCoords } from './util'; // Activation events registered on the root element of each instance for activation var ACTIVATION_EVENT_TYPES = [ 'touchstart', 'pointerdown', 'mousedown', 'keydown', ]; // Deactivation events registered on documentElement when a pointer-related down event occurs var POINTER_DEACTIVATION_EVENT_TYPES = [ 'touchend', 'pointerup', 'mouseup', 'contextmenu', ]; // simultaneous nested activations var activatedTargets = []; var MDCRippleFoundation = /** @class */ (function (_super) { __extends(MDCRippleFoundation, _super); function MDCRippleFoundation(adapter) { var _this = _super.call(this, __assign(__assign({}, MDCRippleFoundation.defaultAdapter), adapter)) || this; _this.activationAnimationHasEnded = false; _this.activationTimer = 0; _this.fgDeactivationRemovalTimer = 0; _this.fgScale = '0'; _this.frame = { width: 0, height: 0 }; _this.initialSize = 0; _this.layoutFrame = 0; _this.maxRadius = 0; _this.unboundedCoords = { left: 0, top: 0 }; _this.activationState = _this.defaultActivationState(); _this.activationTimerCallback = function () { _this.activationAnimationHasEnded = true; _this.runDeactivationUXLogicIfReady(); }; _this.activateHandler = function (e) { _this.activateImpl(e); }; _this.deactivateHandler = function () { _this.deactivateImpl(); }; _this.focusHandler = function () { _this.handleFocus(); }; _this.blurHandler = function () { _this.handleBlur(); }; _this.resizeHandler = function () { _this.layout(); }; return _this; } Object.defineProperty(MDCRippleFoundation, "cssClasses", { get: function () { return cssClasses; }, enumerable: false, configurable: true }); Object.defineProperty(MDCRippleFoundation, "strings", { get: function () { return strings; }, enumerable: false, configurable: true }); Object.defineProperty(MDCRippleFoundation, "numbers", { get: function () { return numbers; }, enumerable: false, configurable: true }); Object.defineProperty(MDCRippleFoundation, "defaultAdapter", { get: function () { return { addClass: function () { return undefined; }, browserSupportsCssVars: function () { return true; }, computeBoundingRect: function () { return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }); }, containsEventTarget: function () { return true; }, deregisterDocumentInteractionHandler: function () { return undefined; }, deregisterInteractionHandler: function () { return undefined; }, deregisterResizeHandler: function () { return undefined; }, getWindowPageOffset: function () { return ({ x: 0, y: 0 }); }, isSurfaceActive: function () { return true; }, isSurfaceDisabled: function () { return true; }, isUnbounded: function () { return true; }, registerDocumentInteractionHandler: function () { return undefined; }, registerInteractionHandler: function () { return undefined; }, registerResizeHandler: function () { return undefined; }, removeClass: function () { return undefined; }, updateCssVariable: function () { return undefined; }, }; }, enumerable: false, configurable: true }); MDCRippleFoundation.prototype.init = function () { var _this = this; var supportsPressRipple = this.supportsPressRipple(); this.registerRootHandlers(supportsPressRipple); if (supportsPressRipple) { var _a = MDCRippleFoundation.cssClasses, ROOT_1 = _a.ROOT, UNBOUNDED_1 = _a.UNBOUNDED; requestAnimationFrame(function () { _this.adapter.addClass(ROOT_1); if (_this.adapter.isUnbounded()) { _this.adapter.addClass(UNBOUNDED_1); // Unbounded ripples need layout logic applied immediately to set coordinates for both shade and ripple _this.layoutInternal(); } }); } }; MDCRippleFoundation.prototype.destroy = function () { var _this = this; if (this.supportsPressRipple()) { if (this.activationTimer) { clearTimeout(this.activationTimer); this.activationTimer = 0; this.adapter.removeClass(MDCRippleFoundation.cssClasses.FG_ACTIVATION); } if (this.fgDeactivationRemovalTimer) { clearTimeout(this.fgDeactivationRemovalTimer); this.fgDeactivationRemovalTimer = 0; this.adapter.removeClass(MDCRippleFoundation.cssClasses.FG_DEACTIVATION); } var _a = MDCRippleFoundation.cssClasses, ROOT_2 = _a.ROOT, UNBOUNDED_2 = _a.UNBOUNDED; requestAnimationFrame(function () { _this.adapter.removeClass(ROOT_2); _this.adapter.removeClass(UNBOUNDED_2); _this.removeCssVars(); }); } this.deregisterRootHandlers(); this.deregisterDeactivationHandlers(); }; /** * @param evt Optional event containing position information. */ MDCRippleFoundation.prototype.activate = function (evt) { this.activateImpl(evt); }; MDCRippleFoundation.prototype.deactivate = function () { this.deactivateImpl(); }; MDCRippleFoundation.prototype.layout = function () { var _this = this; if (this.layoutFrame) { cancelAnimationFrame(this.layoutFrame); } this.layoutFrame = requestAnimationFrame(function () { _this.layoutInternal(); _this.layoutFrame = 0; }); }; MDCRippleFoundation.prototype.setUnbounded = function (unbounded) { var UNBOUNDED = MDCRippleFoundation.cssClasses.UNBOUNDED; if (unbounded) { this.adapter.addClass(UNBOUNDED); } else { this.adapter.removeClass(UNBOUNDED); } }; MDCRippleFoundation.prototype.handleFocus = function () { var _this = this; requestAnimationFrame(function () { return _this.adapter.addClass(MDCRippleFoundation.cssClasses.BG_FOCUSED); }); }; MDCRippleFoundation.prototype.handleBlur = function () { var _this = this; requestAnimationFrame(function () { return _this.adapter.removeClass(MDCRippleFoundation.cssClasses.BG_FOCUSED); }); }; /** * We compute this property so that we are not querying information about the client * until the point in time where the foundation requests it. This prevents scenarios where * client-side feature-detection may happen too early, such as when components are rendered on the server * and then initialized at mount time on the client. */ MDCRippleFoundation.prototype.supportsPressRipple = function () { return this.adapter.browserSupportsCssVars(); }; MDCRippleFoundation.prototype.defaultActivationState = function () { return { activationEvent: undefined, hasDeactivationUXRun: false, isActivated: false, isProgrammatic: false, wasActivatedByPointer: false, wasElementMadeActive: false, }; }; /** * supportsPressRipple Passed from init to save a redundant function call */ MDCRippleFoundation.prototype.registerRootHandlers = function (supportsPressRipple) { var e_1, _a; if (supportsPressRipple) { try { for (var ACTIVATION_EVENT_TYPES_1 = __values(ACTIVATION_EVENT_TYPES), ACTIVATION_EVENT_TYPES_1_1 = ACTIVATION_EVENT_TYPES_1.next(); !ACTIVATION_EVENT_TYPES_1_1.done; ACTIVATION_EVENT_TYPES_1_1 = ACTIVATION_EVENT_TYPES_1.next()) { var evtType = ACTIVATION_EVENT_TYPES_1_1.value; this.adapter.registerInteractionHandler(evtType, this.activateHandler); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (ACTIVATION_EVENT_TYPES_1_1 && !ACTIVATION_EVENT_TYPES_1_1.done && (_a = ACTIVATION_EVENT_TYPES_1.return)) _a.call(ACTIVATION_EVENT_TYPES_1); } finally { if (e_1) throw e_1.error; } } if (this.adapter.isUnbounded()) { this.adapter.registerResizeHandler(this.resizeHandler); } } this.adapter.registerInteractionHandler('focus', this.focusHandler); this.adapter.registerInteractionHandler('blur', this.blurHandler); }; MDCRippleFoundation.prototype.registerDeactivationHandlers = function (evt) { var e_2, _a; if (evt.type === 'keydown') { this.adapter.registerInteractionHandler('keyup', this.deactivateHandler); } else { try { for (var POINTER_DEACTIVATION_EVENT_TYPES_1 = __values(POINTER_DEACTIVATION_EVENT_TYPES), POINTER_DEACTIVATION_EVENT_TYPES_1_1 = POINTER_DEACTIVATION_EVENT_TYPES_1.next(); !POINTER_DEACTIVATION_EVENT_TYPES_1_1.done; POINTER_DEACTIVATION_EVENT_TYPES_1_1 = POINTER_DEACTIVATION_EVENT_TYPES_1.next()) { var evtType = POINTER_DEACTIVATION_EVENT_TYPES_1_1.value; this.adapter.registerDocumentInteractionHandler(evtType, this.deactivateHandler); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (POINTER_DEACTIVATION_EVENT_TYPES_1_1 && !POINTER_DEACTIVATION_EVENT_TYPES_1_1.done && (_a = POINTER_DEACTIVATION_EVENT_TYPES_1.return)) _a.call(POINTER_DEACTIVATION_EVENT_TYPES_1); } finally { if (e_2) throw e_2.error; } } } }; MDCRippleFoundation.prototype.deregisterRootHandlers = function () { var e_3, _a; try { for (var ACTIVATION_EVENT_TYPES_2 = __values(ACTIVATION_EVENT_TYPES), ACTIVATION_EVENT_TYPES_2_1 = ACTIVATION_EVENT_TYPES_2.next(); !ACTIVATION_EVENT_TYPES_2_1.done; ACTIVATION_EVENT_TYPES_2_1 = ACTIVATION_EVENT_TYPES_2.next()) { var evtType = ACTIVATION_EVENT_TYPES_2_1.value; this.adapter.deregisterInteractionHandler(evtType, this.activateHandler); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (ACTIVATION_EVENT_TYPES_2_1 && !ACTIVATION_EVENT_TYPES_2_1.done && (_a = ACTIVATION_EVENT_TYPES_2.return)) _a.call(ACTIVATION_EVENT_TYPES_2); } finally { if (e_3) throw e_3.error; } } this.adapter.deregisterInteractionHandler('focus', this.focusHandler); this.adapter.deregisterInteractionHandler('blur', this.blurHandler); if (this.adapter.isUnbounded()) { this.adapter.deregisterResizeHandler(this.resizeHandler); } }; MDCRippleFoundation.prototype.deregisterDeactivationHandlers = function () { var e_4, _a; this.adapter.deregisterInteractionHandler('keyup', this.deactivateHandler); try { for (var POINTER_DEACTIVATION_EVENT_TYPES_2 = __values(POINTER_DEACTIVATION_EVENT_TYPES), POINTER_DEACTIVATION_EVENT_TYPES_2_1 = POINTER_DEACTIVATION_EVENT_TYPES_2.next(); !POINTER_DEACTIVATION_EVENT_TYPES_2_1.done; POINTER_DEACTIVATION_EVENT_TYPES_2_1 = POINTER_DEACTIVATION_EVENT_TYPES_2.next()) { var evtType = POINTER_DEACTIVATION_EVENT_TYPES_2_1.value; this.adapter.deregisterDocumentInteractionHandler(evtType, this.deactivateHandler); } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (POINTER_DEACTIVATION_EVENT_TYPES_2_1 && !POINTER_DEACTIVATION_EVENT_TYPES_2_1.done && (_a = POINTER_DEACTIVATION_EVENT_TYPES_2.return)) _a.call(POINTER_DEACTIVATION_EVENT_TYPES_2); } finally { if (e_4) throw e_4.error; } } }; MDCRippleFoundation.prototype.removeCssVars = function () { var _this = this; var rippleStrings = MDCRippleFoundation.strings; var keys = Object.keys(rippleStrings); keys.forEach(function (key) { if (key.indexOf('VAR_') === 0) { _this.adapter.updateCssVariable(rippleStrings[key], null); } }); }; MDCRippleFoundation.prototype.activateImpl = function (evt) { var _this = this; if (this.adapter.isSurfaceDisabled()) { return; } var activationState = this.activationState; if (activationState.isActivated) { return; } // Avoid reacting to follow-on events fired by touch device after an already-processed user interaction var previousActivationEvent = this.previousActivationEvent; var isSameInteraction = previousActivationEvent && evt !== undefined && previousActivationEvent.type !== evt.type; if (isSameInteraction) { return; } activationState.isActivated = true; activationState.isProgrammatic = evt === undefined; activationState.activationEvent = evt; activationState.wasActivatedByPointer = activationState.isProgrammatic ? false : evt !== undefined && (evt.type === 'mousedown' || evt.type === 'touchstart' || evt.type === 'pointerdown'); var hasActivatedChild = evt !== undefined && activatedTargets.length > 0 && activatedTargets.some(function (target) { return _this.adapter.containsEventTarget(target); }); if (hasActivatedChild) { // Immediately reset activation state, while preserving logic that prevents touch follow-on events this.resetActivationState(); return; } if (evt !== undefined) { activatedTargets.push(evt.target); this.registerDeactivationHandlers(evt); } activationState.wasElementMadeActive = this.checkElementMadeActive(evt); if (activationState.wasElementMadeActive) { this.animateActivation(); } requestAnimationFrame(function () { // Reset array on next frame after the current event has had a chance to bubble to prevent ancestor ripples activatedTargets = []; if (!activationState.wasElementMadeActive && evt !== undefined && (evt.key === ' ' || evt.keyCode === 32)) { // If space was pressed, try again within an rAF call to detect :active, because different UAs report // active states inconsistently when they're called within event handling code: // - https://bugs.chromium.org/p/chromium/issues/detail?id=635971 // - https://bugzilla.mozilla.org/show_bug.cgi?id=1293741 // We try first outside rAF to support Edge, which does not exhibit this problem, but will crash if a CSS // variable is set within a rAF callback for a submit button interaction (#2241). activationState.wasElementMadeActive = _this.checkElementMadeActive(evt); if (activationState.wasElementMadeActive) { _this.animateActivation(); } } if (!activationState.wasElementMadeActive) { // Reset activation state immediately if element was not made active. _this.activationState = _this.defaultActivationState(); } }); }; MDCRippleFoundation.prototype.checkElementMadeActive = function (evt) { return (evt !== undefined && evt.type === 'keydown') ? this.adapter.isSurfaceActive() : true; }; MDCRippleFoundation.prototype.animateActivation = function () { var _this = this; var _a = MDCRippleFoundation.strings, VAR_FG_TRANSLATE_START = _a.VAR_FG_TRANSLATE_START, VAR_FG_TRANSLATE_END = _a.VAR_FG_TRANSLATE_END; var _b = MDCRippleFoundation.cssClasses, FG_DEACTIVATION = _b.FG_DEACTIVATION, FG_ACTIVATION = _b.FG_ACTIVATION; var DEACTIVATION_TIMEOUT_MS = MDCRippleFoundation.numbers.DEACTIVATION_TIMEOUT_MS; this.layoutInternal(); var translateStart = ''; var translateEnd = ''; if (!this.adapter.isUnbounded()) { var _c = this.getFgTranslationCoordinates(), startPoint = _c.startPoint, endPoint = _c.endPoint; translateStart = startPoint.x + "px, " + startPoint.y + "px"; translateEnd = endPoint.x + "px, " + endPoint.y + "px"; } this.adapter.updateCssVariable(VAR_FG_TRANSLATE_START, translateStart); this.adapter.updateCssVariable(VAR_FG_TRANSLATE_END, translateEnd); // Cancel any ongoing activation/deactivation animations clearTimeout(this.activationTimer); clearTimeout(this.fgDeactivationRemovalTimer); this.rmBoundedActivationClasses(); this.adapter.removeClass(FG_DEACTIVATION); // Force layout in order to re-trigger the animation. this.adapter.computeBoundingRect(); this.adapter.addClass(FG_ACTIVATION); this.activationTimer = setTimeout(function () { _this.activationTimerCallback(); }, DEACTIVATION_TIMEOUT_MS); }; MDCRippleFoundation.prototype.getFgTranslationCoordinates = function () { var _a = this.activationState, activationEvent = _a.activationEvent, wasActivatedByPointer = _a.wasActivatedByPointer; var startPoint; if (wasActivatedByPointer) { startPoint = getNormalizedEventCoords(activationEvent, this.adapter.getWindowPageOffset(), this.adapter.computeBoundingRect()); } else { startPoint = { x: this.frame.width / 2, y: this.frame.height / 2, }; } // Center the element around the start point. startPoint = { x: startPoint.x - (this.initialSize / 2), y: startPoint.y - (this.initialSize / 2), }; var endPoint = { x: (this.frame.width / 2) - (this.initialSize / 2), y: (this.frame.height / 2) - (this.initialSize / 2), }; return { startPoint: startPoint, endPoint: endPoint }; }; MDCRippleFoundation.prototype.runDeactivationUXLogicIfReady = function () { var _this = this; // This method is called both when a pointing device is released, and when the activation animation ends. // The deactivation animation should only run after both of those occur. var FG_DEACTIVATION = MDCRippleFoundation.cssClasses.FG_DEACTIVATION; var _a = this.activationState, hasDeactivationUXRun = _a.hasDeactivationUXRun, isActivated = _a.isActivated; var activationHasEnded = hasDeactivationUXRun || !isActivated; if (activationHasEnded && this.activationAnimationHasEnded) { this.rmBoundedActivationClasses(); this.adapter.addClass(FG_DEACTIVATION); this.fgDeactivationRemovalTimer = setTimeout(function () { _this.adapter.removeClass(FG_DEACTIVATION); }, numbers.FG_DEACTIVATION_MS); } }; MDCRippleFoundation.prototype.rmBoundedActivationClasses = function () { var FG_ACTIVATION = MDCRippleFoundation.cssClasses.FG_ACTIVATION; this.adapter.removeClass(FG_ACTIVATION); this.activationAnimationHasEnded = false; this.adapter.computeBoundingRect(); }; MDCRippleFoundation.prototype.resetActivationState = function () { var _this = this; this.previousActivationEvent = this.activationState.activationEvent; this.activationState = this.defaultActivationState(); // Touch devices may fire additional events for the same interaction within a short time. // Store the previous event until it's safe to assume that subsequent events are for new interactions. setTimeout(function () { return _this.previousActivationEvent = undefined; }, MDCRippleFoundation.numbers.TAP_DELAY_MS); }; MDCRippleFoundation.prototype.deactivateImpl = function () { var _this = this; var activationState = this.activationState; // This can happen in scenarios such as when you have a keyup event that blurs the element. if (!activationState.isActivated) { return; } var state = __assign({}, activationState); if (activationState.isProgrammatic) { requestAnimationFrame(function () { _this.animateDeactivation(state); }); this.resetActivationState(); } else { this.deregisterDeactivationHandlers(); requestAnimationFrame(function () { _this.activationState.hasDeactivationUXRun = true; _this.animateDeactivation(state); _this.resetActivationState(); }); } }; MDCRippleFoundation.prototype.animateDeactivation = function (_a) { var wasActivatedByPointer = _a.wasActivatedByPointer, wasElementMadeActive = _a.wasElementMadeActive; if (wasActivatedByPointer || wasElementMadeActive) { this.runDeactivationUXLogicIfReady(); } }; MDCRippleFoundation.prototype.layoutInternal = function () { var _this = this; this.frame = this.adapter.computeBoundingRect(); var maxDim = Math.max(this.frame.height, this.frame.width); // Surface diameter is treated differently for unbounded vs. bounded ripples. // Unbounded ripple diameter is calculated smaller since the surface is expected to already be padded appropriately // to extend the hitbox, and the ripple is expected to meet the edges of the padded hitbox (which is typically // square). Bounded ripples, on the other hand, are fully expected to expand beyond the surface's longest diameter // (calculated based on the diagonal plus a constant padding), and are clipped at the surface's border via // `overflow: hidden`. var getBoundedRadius = function () { var hypotenuse = Math.sqrt(Math.pow(_this.frame.width, 2) + Math.pow(_this.frame.height, 2)); return hypotenuse + MDCRippleFoundation.numbers.PADDING; }; this.maxRadius = this.adapter.isUnbounded() ? maxDim : getBoundedRadius(); // Ripple is sized as a fraction of the largest dimension of the surface, then scales up using a CSS scale transform var initialSize = Math.floor(maxDim * MDCRippleFoundation.numbers.INITIAL_ORIGIN_SCALE); // Unbounded ripple size should always be even number to equally center align. if (this.adapter.isUnbounded() && initialSize % 2 !== 0) { this.initialSize = initialSize - 1; } else { this.initialSize = initialSize; } this.fgScale = "" + this.maxRadius / this.initialSize; this.updateLayoutCssVars(); }; MDCRippleFoundation.prototype.updateLayoutCssVars = function () { var _a = MDCRippleFoundation.strings, VAR_FG_SIZE = _a.VAR_FG_SIZE, VAR_LEFT = _a.VAR_LEFT, VAR_TOP = _a.VAR_TOP, VAR_FG_SCALE = _a.VAR_FG_SCALE; this.adapter.updateCssVariable(VAR_FG_SIZE, this.initialSize + "px"); this.adapter.updateCssVariable(VAR_FG_SCALE, this.fgScale); if (this.adapter.isUnbounded()) { this.unboundedCoords = { left: Math.round((this.frame.width / 2) - (this.initialSize / 2)), top: Math.round((this.frame.height / 2) - (this.initialSize / 2)), }; this.adapter.updateCssVariable(VAR_LEFT, this.unboundedCoords.left + "px"); this.adapter.updateCssVariable(VAR_TOP, this.unboundedCoords.top + "px"); } }; return MDCRippleFoundation; }(MDCFoundation)); export { MDCRippleFoundation }; // tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier. export default MDCRippleFoundation; //# sourceMappingURL=foundation.js.map