use-wave
Version:
The material-ripple hook for React that actually works
166 lines (156 loc) • 6.29 kB
JavaScript
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;