@limetech/mdc-ripple
Version:
The Material Components for the web Ink Ripple effect for web element interactions
468 lines • 23.4 kB
JavaScript
/**
* @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 * as tslib_1 from "tslib";
import { MDCFoundation } from '@limetech/mdc-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) {
tslib_1.__extends(MDCRippleFoundation, _super);
function MDCRippleFoundation(adapter) {
var _this = _super.call(this, tslib_1.__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) { return _this.activate_(e); };
_this.deactivateHandler_ = function () { return _this.deactivate_(); };
_this.focusHandler_ = function () { return _this.handleFocus(); };
_this.blurHandler_ = function () { return _this.handleBlur(); };
_this.resizeHandler_ = function () { return _this.layout(); };
return _this;
}
Object.defineProperty(MDCRippleFoundation, "cssClasses", {
get: function () {
return cssClasses;
},
enumerable: true,
configurable: true
});
Object.defineProperty(MDCRippleFoundation, "strings", {
get: function () {
return strings;
},
enumerable: true,
configurable: true
});
Object.defineProperty(MDCRippleFoundation, "numbers", {
get: function () {
return numbers;
},
enumerable: true,
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: true,
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.activate_(evt);
};
MDCRippleFoundation.prototype.deactivate = function () {
this.deactivate_();
};
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 _this = this;
if (supportsPressRipple) {
ACTIVATION_EVENT_TYPES.forEach(function (evtType) {
_this.adapter_.registerInteractionHandler(evtType, _this.activateHandler_);
});
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 _this = this;
if (evt.type === 'keydown') {
this.adapter_.registerInteractionHandler('keyup', this.deactivateHandler_);
}
else {
POINTER_DEACTIVATION_EVENT_TYPES.forEach(function (evtType) {
_this.adapter_.registerDocumentInteractionHandler(evtType, _this.deactivateHandler_);
});
}
};
MDCRippleFoundation.prototype.deregisterRootHandlers_ = function () {
var _this = this;
ACTIVATION_EVENT_TYPES.forEach(function (evtType) {
_this.adapter_.deregisterInteractionHandler(evtType, _this.activateHandler_);
});
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 _this = this;
this.adapter_.deregisterInteractionHandler('keyup', this.deactivateHandler_);
POINTER_DEACTIVATION_EVENT_TYPES.forEach(function (evtType) {
_this.adapter_.deregisterDocumentInteractionHandler(evtType, _this.deactivateHandler_);
});
};
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.activate_ = 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 () { return _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.deactivate_ = 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 = tslib_1.__assign({}, activationState);
if (activationState.isProgrammatic) {
requestAnimationFrame(function () { return _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