diff --git a/package.json b/package.json index ff4386b1067..5f9499c4894 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "closureWhitelist": [ "mdc-animation", "mdc-base", - "mdc-menu" + "mdc-menu", + "mdc-ripple" ] } diff --git a/packages/mdc-ripple/adapter.js b/packages/mdc-ripple/adapter.js new file mode 100644 index 00000000000..367dfb8732f --- /dev/null +++ b/packages/mdc-ripple/adapter.js @@ -0,0 +1,94 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/** + * Adapter for MDC Ripple. Provides an interface for managing + * - classes + * - dom + * - CSS variables + * - position + * - dimensions + * - scroll position + * - event handlers + * - unbounded, active and disabled states + * + * Additionally, provides type information for the adapter to the Closure + * compiler. + * + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/architecture.md + * + * @record + */ +export default class MDCRippleAdapter { + + /** @return {boolean} */ + browserSupportsCssVars() {} + + /** @return {boolean} */ + isUnbounded() {} + + /** @return {boolean} */ + isSurfaceActive() {} + + /** @return {boolean} */ + isSurfaceDisabled() {} + + /** @param {string} className */ + addClass(className) {} + + /** @param {string} className */ + removeClass(className) {} + + /** + * @param {string} evtType + * @param {!Function} handler + */ + registerInteractionHandler(evtType, handler) {} + + /** + * @param {string} evtType + * @param {!Function} handler + */ + deregisterInteractionHandler(evtType, handler) {} + + /** + * @param {!Function} handler + */ + registerResizeHandler(handler) {} + + /** + * @param {!Function} handler + */ + deregisterResizeHandler(handler) {} + + /** + * @param {string} varName + * @param {?number|string} value + */ + updateCssVariable(varName, value) {} + + /** @return {!ClientRect} */ + computeBoundingRect() {} + + /** @return {{x: number, y: number}} */ + getWindowPageOffset() {} + +} diff --git a/packages/mdc-ripple/foundation.js b/packages/mdc-ripple/foundation.js index 866cfdb081b..e63ba2ab595 100644 --- a/packages/mdc-ripple/foundation.js +++ b/packages/mdc-ripple/foundation.js @@ -14,11 +14,55 @@ * limitations under the License. */ -import {MDCFoundation} from '@material/base'; - +import MDCFoundation from '@material/base/foundation'; +import MDCRippleAdapter from './adapter'; import {cssClasses, strings, numbers} from './constants'; import {getNormalizedEventCoords} from './util'; +/** + * @typedef {!{ + * isActivated: (boolean|undefined), + * hasDeactivationUXRun: (boolean|undefined), + * wasActivatedByPointer: (boolean|undefined), + * wasElementMadeActive: (boolean|undefined), + * activationStartTime: (number|undefined), + * activationEvent: Event, + * isProgrammatic: (boolean|undefined) + * }} + */ +let ActivationStateType; + +/** + * @typedef {!{ + * activate: (string|undefined), + * deactivate: (string|undefined), + * focus: (string|undefined), + * blur: (string|undefined) + * }} + */ +let ListenerInfoType; + +/** + * @typedef {!{ + * activate: function(!Event), + * deactivate: function(!Event), + * focus: function(), + * blur: function() + * }} + */ +let ListenersType; + +/** + * @typedef {!{ + * x: number, + * y: number + * }} + */ +let PointType; + +/** + * @enum {string} + */ const DEACTIVATION_ACTIVATION_PAIRS = { mouseup: 'mousedown', pointerup: 'pointerdown', @@ -27,6 +71,9 @@ const DEACTIVATION_ACTIVATION_PAIRS = { blur: 'focus', }; +/** + * @extends {MDCFoundation} + */ export default class MDCRippleFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; @@ -58,10 +105,13 @@ export default class MDCRippleFoundation extends MDCFoundation { }; } - // 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. + /** + * 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. + * @return {boolean} + */ get isSupported_() { return this.adapter_.browserSupportsCssVars(); } @@ -69,12 +119,25 @@ export default class MDCRippleFoundation extends MDCFoundation { constructor(adapter) { super(Object.assign(MDCRippleFoundation.defaultAdapter, adapter)); + /** @private {number} */ this.layoutFrame_ = 0; - this.frame_ = {width: 0, height: 0}; + + /** @private {!ClientRect} */ + this.frame_ = /** @type {!ClientRect} */ ({width: 0, height: 0}); + + /** @private {!ActivationStateType} */ this.activationState_ = this.defaultActivationState_(); + + /** @private {number} */ this.xfDuration_ = 0; + + /** @private {number} */ this.initialSize_ = 0; + + /** @private {number} */ this.maxRadius_ = 0; + + /** @private {!Array<{ListenerInfoType}>} */ this.listenerInfos_ = [ {activate: 'touchstart', deactivate: 'touchend'}, {activate: 'pointerdown', deactivate: 'pointerup'}, @@ -82,6 +145,8 @@ export default class MDCRippleFoundation extends MDCFoundation { {activate: 'keydown', deactivate: 'keyup'}, {focus: 'focus', blur: 'blur'}, ]; + + /** @private {!ListenersType} */ this.listeners_ = { activate: (e) => this.activate_(e), deactivate: (e) => this.deactivate_(e), @@ -92,21 +157,38 @@ export default class MDCRippleFoundation extends MDCFoundation { () => this.adapter_.removeClass(MDCRippleFoundation.cssClasses.BG_FOCUSED) ), }; + + /** @private {!Function} */ this.resizeHandler_ = () => this.layout(); + + /** @private {!{left: number, top:number}} */ this.unboundedCoords_ = { left: 0, top: 0, }; + + /** @private {number} */ this.fgScale_ = 0; + + /** @private {number} */ this.activationTimer_ = 0; + + /** @private {number} */ this.fgDeactivationRemovalTimer_ = 0; + + /** @private {boolean} */ this.activationAnimationHasEnded_ = false; + + /** @private {!Function} */ this.activationTimerCallback_ = () => { this.activationAnimationHasEnded_ = true; this.runDeactivationUXLogicIfReady_(); }; } + /** + * @return {!ActivationStateType} + */ defaultActivationState_() { return { isActivated: false, @@ -135,6 +217,7 @@ export default class MDCRippleFoundation extends MDCFoundation { }); } + /** @private */ addEventListeners_() { this.listenerInfos_.forEach((info) => { Object.keys(info).forEach((k) => { @@ -144,6 +227,10 @@ export default class MDCRippleFoundation extends MDCFoundation { this.adapter_.registerResizeHandler(this.resizeHandler_); } + /** + * @param {Event} e + * @private + */ activate_(e) { if (this.adapter_.isSurfaceDisabled()) { return; @@ -182,6 +269,7 @@ export default class MDCRippleFoundation extends MDCFoundation { this.activate_(null); } + /** @private */ animateActivation_() { const {VAR_FG_TRANSLATE_START, VAR_FG_TRANSLATE_END} = MDCRippleFoundation.strings; const { @@ -215,6 +303,10 @@ export default class MDCRippleFoundation extends MDCFoundation { this.activationTimer_ = setTimeout(() => this.activationTimerCallback_(), DEACTIVATION_TIMEOUT_MS); } + /** + * @private + * @return {{startPoint: PointType, endPoint: PointType}} + */ getFgTranslationCoordinates_() { const {activationState_: activationState} = this; const {activationEvent, wasActivatedByPointer} = activationState; @@ -222,7 +314,8 @@ export default class MDCRippleFoundation extends MDCFoundation { let startPoint; if (wasActivatedByPointer) { startPoint = getNormalizedEventCoords( - activationEvent, this.adapter_.getWindowPageOffset(), this.adapter_.computeBoundingRect() + /** @type {!Event} */ (activationEvent), + this.adapter_.getWindowPageOffset(), this.adapter_.computeBoundingRect() ); } else { startPoint = { @@ -244,6 +337,7 @@ export default class MDCRippleFoundation extends MDCFoundation { return {startPoint, endPoint}; } + /** @private */ runDeactivationUXLogicIfReady_() { const {FG_DEACTIVATION} = MDCRippleFoundation.cssClasses; const {hasDeactivationUXRun, isActivated} = this.activationState_; @@ -257,6 +351,7 @@ export default class MDCRippleFoundation extends MDCFoundation { } } + /** @private */ rmBoundedActivationClasses_() { const {BG_ACTIVE_FILL, FG_ACTIVATION} = MDCRippleFoundation.cssClasses; this.adapter_.removeClass(BG_ACTIVE_FILL); @@ -265,6 +360,10 @@ export default class MDCRippleFoundation extends MDCFoundation { this.adapter_.computeBoundingRect(); } + /** + * @param {Event} e + * @private + */ deactivate_(e) { const {activationState_: activationState} = this; // This can happen in scenarios such as when you have a keyup event that blurs the element. @@ -274,7 +373,8 @@ export default class MDCRippleFoundation extends MDCFoundation { // Programmatic deactivation. if (activationState.isProgrammatic) { const evtObject = null; - requestAnimationFrame(() => this.animateDeactivation_(evtObject, Object.assign({}, activationState))); + const state = /** @type {!ActivationStateType} */ (Object.assign({}, activationState)); + requestAnimationFrame(() => this.animateDeactivation_(evtObject, state)); this.activationState_ = this.defaultActivationState_(); return; } @@ -291,7 +391,7 @@ export default class MDCRippleFoundation extends MDCFoundation { needsActualDeactivation = e.type === 'mouseup'; } - const state = Object.assign({}, activationState); + const state = /** @type {!ActivationStateType} */ (Object.assign({}, activationState)); requestAnimationFrame(() => { if (needsDeactivationUX) { this.activationState_.hasDeactivationUXRun = true; @@ -308,6 +408,11 @@ export default class MDCRippleFoundation extends MDCFoundation { this.deactivate_(null); } + /** + * @param {Event} e + * @param {!ActivationStateType} options + * @private + */ animateDeactivation_(e, {wasActivatedByPointer, wasElementMadeActive}) { const {BG_FOCUSED} = MDCRippleFoundation.cssClasses; if (wasActivatedByPointer || wasElementMadeActive) { @@ -331,6 +436,7 @@ export default class MDCRippleFoundation extends MDCFoundation { }); } + /** @private */ removeEventListeners_() { this.listenerInfos_.forEach((info) => { Object.keys(info).forEach((k) => { @@ -340,6 +446,7 @@ export default class MDCRippleFoundation extends MDCFoundation { this.adapter_.deregisterResizeHandler(this.resizeHandler_); } + /** @private */ removeCssVars_() { const {strings} = MDCRippleFoundation; Object.keys(strings).forEach((k) => { @@ -359,6 +466,7 @@ export default class MDCRippleFoundation extends MDCFoundation { }); } + /** @private */ layoutInternal_() { this.frame_ = this.adapter_.computeBoundingRect(); @@ -375,6 +483,7 @@ export default class MDCRippleFoundation extends MDCFoundation { this.updateLayoutCssVars_(); } + /** @private */ updateLayoutCssVars_() { const { VAR_SURFACE_WIDTH, VAR_SURFACE_HEIGHT, VAR_FG_SIZE, diff --git a/packages/mdc-ripple/index.js b/packages/mdc-ripple/index.js index 4d7ac73d829..7b63ca57c71 100644 --- a/packages/mdc-ripple/index.js +++ b/packages/mdc-ripple/index.js @@ -14,23 +14,47 @@ * limitations under the License. */ -import {MDCComponent} from '@material/base'; +import MDCComponent from '@material/base/component'; +import MDCRippleAdapter from './adapter'; import MDCRippleFoundation from './foundation'; import * as util from './util'; export {MDCRippleFoundation}; export {util}; +/** + * @extends MDCComponent + */ export class MDCRipple extends MDCComponent { + /** @param {...?} args */ + constructor(...args) { + super(...args); + + /** @type {boolean} */ + this.disabled = false; + + /** @private {boolean} */ + this.unbounded_; + } + + /** + * @param {!Element} root + * @param {{isUnbounded: (boolean|undefined)}=} options + * @return {!MDCRipple} + */ static attachTo(root, {isUnbounded = undefined} = {}) { const ripple = new MDCRipple(root); // Only override unbounded behavior if option is explicitly specified if (isUnbounded !== undefined) { - ripple.unbounded = isUnbounded; + ripple.unbounded = /** @type {boolean} */ (isUnbounded); } return ripple; } + /** + * @param {!MDCRipple} instance + * @return {!MDCRippleAdapter} + */ static createAdapter(instance) { const MATCHES = util.getMatchesProperty(HTMLElement.prototype); @@ -53,10 +77,12 @@ export class MDCRipple extends MDCComponent { }; } + /** @return {boolean} */ get unbounded() { return this.unbounded_; } + /** @param {boolean} unbounded */ set unbounded(unbounded) { const {UNBOUNDED} = MDCRippleFoundation.cssClasses; this.unbounded_ = Boolean(unbounded); @@ -79,6 +105,7 @@ export class MDCRipple extends MDCComponent { this.foundation_.layout(); } + /** @return {!MDCRippleFoundation} */ getDefaultFoundation() { return new MDCRippleFoundation(MDCRipple.createAdapter(this)); } diff --git a/packages/mdc-ripple/util.js b/packages/mdc-ripple/util.js index 721312efb41..a919865e7dd 100644 --- a/packages/mdc-ripple/util.js +++ b/packages/mdc-ripple/util.js @@ -14,8 +14,13 @@ * limitations under the License. */ +/** @private {boolean|undefined} */ let supportsPassive_; +/** + * @param {!Window} windowObj + * @return {boolean|undefined} + */ export function supportsCssVariables(windowObj) { const supportsFunctionPresent = windowObj.CSS && typeof windowObj.CSS.supports === 'function'; if (!supportsFunctionPresent) { @@ -32,7 +37,13 @@ export function supportsCssVariables(windowObj) { return explicitlySupportsCssVars || weAreFeatureDetectingSafari10plus; } -// Determine whether the current browser supports passive event listeners, and if so, use them. +// +/** + * Determine whether the current browser supports passive event listeners, and if so, use them. + * @param {!Window=} globalObj + * @param {boolean=} forceRefresh + * @return {boolean|{passive: boolean}} + */ export function applyPassive(globalObj = window, forceRefresh = false) { if (supportsPassive_ === undefined || forceRefresh) { let isSupported = false; @@ -48,12 +59,22 @@ export function applyPassive(globalObj = window, forceRefresh = false) { return supportsPassive_ ? {passive: true} : false; } +/** + * @param {!Object} HTMLElementPrototype + * @return {!Array} + */ export function getMatchesProperty(HTMLElementPrototype) { return [ 'webkitMatchesSelector', 'msMatchesSelector', 'matches', ].filter((p) => p in HTMLElementPrototype).pop(); } +/** + * @param {!Event} ev + * @param {!{x: number, y: number}} pageOffset + * @param {!ClientRect} clientRect + * @return {!{x: number, y: number}} + */ export function getNormalizedEventCoords(ev, pageOffset, clientRect) { const {x, y} = pageOffset; const documentX = x + clientRect.left;