UNPKG

use-ripple-hook

Version:

Customizable, lightweight React hook for implementing Google's Material UI style ripple effect

173 lines (172 loc) 6.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useRipple = useRipple; exports.customRipple = customRipple; const react_1 = require("react"); const self = () => document; const completedFactor = 0.4; const className = "__useRipple--ripple"; const containerClassName = "__useRipple--ripple-container"; /** * useRipple - Material UI style ripple effect React hook * @author Jonathan Asplund <jonathan@asplund.net> * @param inputOptions Ripple options * @returns Tuple `[ref, event]`. See https://github.com/asplunds/use-ripple for usage */ function useRipple(inputOptions) { const internalRef = (0, react_1.useRef)(null); const { ref, ...options } = { duration: 450, color: "rgba(255, 255, 255, .3)", cancelAutomatically: false, timingFunction: "cubic-bezier(.42,.36,.28,.88)", disabled: false, className, containerClassName, ignoreNonLeftClick: true, ref: internalRef, ...inputOptions, }; const event = (0, react_1.useCallback)((event) => { if (!ref.current || options.disabled || (options.ignoreNonLeftClick && event.nativeEvent?.which !== 1 && event.nativeEvent?.type === "mousedown")) return; const target = ref.current; if (window.getComputedStyle(target).position === "static") void applyStyles([["position", "relative"]], target); if (!target) return; const existingContainer = target.querySelector(`:scope > .${options.containerClassName}`); const container = existingContainer ?? createRippleContainer(options.containerClassName); if (!existingContainer) target.appendChild(container); // Used to ensure overflow: hidden is registered properly on IOS Safari before ripple is shown void requestAnimationFrame(() => { const begun = Date.now(); const ripple = centerElementToPointer(event, target, createRipple(target, event, options)); const events = ["mouseup", "touchend"]; const cancelRipple = () => { const now = Date.now(); const diff = now - begun; // Ensure the transform animation is complete before cancellation void setTimeout(() => { void cancelRippleAnimation(ripple, options); }, diff > 0.4 * options.duration ? 0 : completedFactor * options.duration - diff); for (const event of events) void self().removeEventListener(event, cancelRipple); }; if (!options.cancelAutomatically && !isTouchDevice()) for (const event of events) void self().addEventListener(event, cancelRipple); else setTimeout(() => void cancelRippleAnimation(ripple, options), options.duration * completedFactor); void container.appendChild(ripple); void options.onSpawn?.({ ripple, cancelRipple, event, ref, container, }); }); }, [ref, options]); return [ref, event]; } /** * HOF useRipple - Generate a custom ripple hook with predefined options * * After generating a HOF useRipple you can then override some or all predefined options by passing a new option object. * @author Jonathan Asplund <jonathan@asplund.net> * @param inputOptions ripple options * @returns Custom HOC useRipple hook */ function customRipple(inputOptions) { return (overrideOptions) => useRipple({ ...inputOptions, ...overrideOptions, }); } function centerElementToPointer(event, ref, element) { const { top, left } = ref.getBoundingClientRect(); void element.style.setProperty("top", px(event.clientY - top)); void element.style.setProperty("left", px(event.clientX - left)); return element; } function px(arg) { return `${arg}px`; } function createRipple(ref, event, { duration, color, timingFunction, className }, ctx = document) { const element = ctx.createElement("div"); const { clientX, clientY } = event; const { height, width, top, left } = ref.getBoundingClientRect(); const maxHeight = Math.max(clientY - top, height - clientY + top); const maxWidth = Math.max(clientX - left, width - clientX + left); // @ts-ignore const size = px(Math.hypot(maxHeight, maxWidth) * 2); const styles = [ ["position", "absolute"], ["height", size], ["width", size], ["transform", "translate(-50%, -50%) scale(0)"], ["pointer-events", "none"], ["border-radius", "50%"], ["opacity", ".6"], ["background", color], [ "transition", `transform ${duration * 0.6}ms ${timingFunction}, opacity ${Math.max(duration * 0.05, 140)}ms ease-out`, ], ]; void element.classList.add(className); void window.requestAnimationFrame(() => { void applyStyles([["transform", "translate(-50%, -50%) scale(1)"]], element); }); return applyStyles(styles, element); } function applyStyles(styles, target) { if (!target) return target; for (const [property, value] of styles) { void target.style.setProperty(property, value); } return target; } function cancelRippleAnimation(element, options) { const { duration, timingFunction } = options; void applyStyles([ ["opacity", "0"], [ "transition", `transform ${duration * 0.6}ms ${timingFunction}, opacity ${duration * 0.65}ms ease-in-out ${duration * 0.13}ms`, ], ], element); void window.requestAnimationFrame(() => { void element.addEventListener("transitionend", (e) => { if (e.propertyName === "opacity") void element.remove(); }); }); } function createRippleContainer(className) { const container = self().createElement("div"); void container.classList.add(className); return applyStyles([ ["position", "absolute"], ["height", "100%"], ["width", "100%"], ["border-radius", "inherit"], ["top", "0"], ["left", "0"], ["pointer-events", "none"], ["overflow", "hidden"], ], container); } /** taken from https://stackoverflow.com/a/4819886/13188385 */ function isTouchDevice() { return ("ontouchstart" in window || navigator.maxTouchPoints > 0 || (navigator?.msMaxTouchPoints ?? 0) > 0); }