UNPKG

use-wave

Version:

The material-ripple hook for React that actually works

166 lines (156 loc) 6.29 kB
const DEFAULT_OPTIONS = { color: 'currentColor', initialOpacity: 0.2, finalOpacity: 0.1, duration: 0.4, easing: 'ease-out', cancellationPeriod: 75 }; /** * Creates a container element for the wave. * @param el The element that the wave will appear on * @param styles The Computed Styles of `el` */ const createContainer = ({ borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius, borderBottomRightRadius }) => { const waveContainer = document.createElement('div'); waveContainer.style.top = '0'; waveContainer.style.left = '0'; waveContainer.style.width = '100%'; waveContainer.style.height = '100%'; waveContainer.style.position = 'absolute'; waveContainer.style.borderRadius = `${borderTopLeftRadius} ${borderTopRightRadius} ${borderBottomRightRadius} ${borderBottomLeftRadius}`; waveContainer.style.overflow = 'hidden'; waveContainer.style.pointerEvents = 'none'; // Meet Safari, the new IE 💩 waveContainer.style.webkitMaskImage = '-webkit-radial-gradient(white, black)'; return waveContainer; }; const createWaveElement = (x, y, size, options) => { const waveElement = document.createElement('div'); waveElement.style.position = 'absolute'; waveElement.style.width = `${size}px`; waveElement.style.height = `${size}px`; waveElement.style.top = `${y}px`; waveElement.style.left = `${x}px`; waveElement.style.background = options.color; waveElement.style.borderRadius = '50%'; waveElement.style.opacity = `${options.initialOpacity}`; waveElement.style.transform = `translate(-50%,-50%) scale(0)`; waveElement.style.transition = `transform ${options.duration}s ${options.easing}, opacity ${options.duration}s ${options.easing}`; return waveElement; }; function magnitude(x1, y1, x2, y2) { const deltaX = x1 - x2; const deltaY = y1 - y2; return Math.sqrt(deltaX * deltaX + deltaY * deltaY); } function getDistanceToFurthestCorner(x, y, { width, height }) { const topLeft = magnitude(x, y, 0, 0); const topRight = magnitude(x, y, width, 0); const bottomLeft = magnitude(x, y, 0, height); const bottomRight = magnitude(x, y, width, height); return Math.max(topLeft, topRight, bottomLeft, bottomRight); } const getRelativePointer = ({ x, y }, { top, left }) => ({ x: x - left, y: y - top }); const WAVE_COUNT = 'useWaveCountInternal'; function incrementWaveCount(el) { const count = getWaveCount(el); setWaveCount(el, count + 1); } function decrementWaveCount(el) { const count = getWaveCount(el); setWaveCount(el, count - 1); } function setWaveCount(el, count) { el.dataset[WAVE_COUNT] = count.toString(); } function getWaveCount(el) { var _a; return parseInt((_a = el.dataset[WAVE_COUNT]) !== null && _a !== void 0 ? _a : '0', 10); } function deleteWaveCount(el) { delete el.dataset[WAVE_COUNT]; } const wave = (event, el, options) => { const rect = el.getBoundingClientRect(); const computedStyles = window.getComputedStyle(el); const { x, y } = getRelativePointer(event, rect); const size = 2.05 * getDistanceToFurthestCorner(x, y, rect); // 2.05 is magic, deal with it. // We're creating a container for the "wave" with `overflow: hidden` // because if we were to set `overflow: hidden` on `el` we // risk altering its appearance. const waveContainer = createContainer(computedStyles); const waveEl = createWaveElement(x, y, size, options); // Keep track of how many waves are active on this element. incrementWaveCount(el); // We reply on absolute positioning, so we need to make sure `el`'s position is non-static let originalPositionValue = ''; if (computedStyles.position === 'static') { if (el.style.position) originalPositionValue = el.style.position; el.style.position = 'relative'; } waveContainer.appendChild(waveEl); el.appendChild(waveContainer); let shouldDissolveWave = false; const releaseWave = (e) => { if (typeof e !== 'undefined') { document.removeEventListener('pointerup', releaseWave); document.removeEventListener('pointercancel', releaseWave); } if (shouldDissolveWave) dissolveWave(); else shouldDissolveWave = true; }; const dissolveWave = () => { waveEl.style.transition = 'opacity 150ms linear'; waveEl.style.opacity = '0'; setTimeout(() => { waveContainer.remove(); decrementWaveCount(el); if (getWaveCount(el) === 0) { deleteWaveCount(el); // Only reset the style after all active waves have been removed el.style.position = originalPositionValue; } }, 150); }; document.addEventListener('pointerup', releaseWave); document.addEventListener('pointercancel', releaseWave); const token = setTimeout(() => { document.removeEventListener('pointercancel', cancelWave); requestAnimationFrame(() => { waveEl.style.transform = `translate(-50%,-50%) scale(1)`; waveEl.style.opacity = `${options.finalOpacity}`; setTimeout(() => releaseWave(), options.duration * 1000); }); }, options.cancellationPeriod); const cancelWave = () => { clearTimeout(token); waveContainer.remove(); document.removeEventListener('pointerup', releaseWave); document.removeEventListener('pointercancel', releaseWave); document.removeEventListener('pointercancel', cancelWave); }; document.addEventListener('pointercancel', cancelWave); }; const elementMap = new WeakMap(); function register(el) { el.addEventListener('pointerdown', (event) => { wave(event, el, Object.assign(Object.assign({}, DEFAULT_OPTIONS), elementMap.get(el))); }); } function useWave(options = {}) { return (el) => { if (!el) return; if (!elementMap.has(el)) register(el); elementMap.set(el, options); }; } export default useWave;