soda-material
Version:
A component library that may follow [material design 3](https://m3.material.io/components) (a.k.a. material you)
104 lines (90 loc) • 3.24 kB
text/typescript
const DatasetName = 'sdRipple' //? dataset name will automatically convert to underscore style
/**
* warn: this is m2 ripple effect
*/
export function rippleEffect<E extends HTMLElement>(
ele: E,
duration = 400,
color = 'rgba(0, 0, 0, 0.1)'
) {
if (
ele.hasAttribute('disabled') ||
Reflect.get(ele, 'disabled') === true ||
ele.dataset[DatasetName] === 'true' ||
ele.dataset.sdDisabled === 'true'
) {
// do not create ripple effect if element is disabled or effect has been attached
return
}
ele.dataset[DatasetName] = 'true'
const onPointerDown = (event: PointerEvent) => {
if (event.button === 2) {
// do not handle right click
return
}
ele.setPointerCapture(event.pointerId) // redirect all event to element
const { x: eleX, y: eleY, width, height } = ele.getBoundingClientRect() // ele pos
const { clientX: pointerX, clientY: pointerY } = event // pointer pos
const rippleX = pointerX - eleX
const rippleY = pointerY - eleY
const radius = Math.max(
Math.hypot(rippleX, rippleY),
Math.hypot(rippleX, height - rippleY),
Math.hypot(width - rippleX, rippleY),
Math.hypot(width - rippleX, height - rippleY)
)
ele.style.position = 'relative'
ele.style.overflow = 'hidden'
const ripple = document.createElement('div')
ripple.style.pointerEvents = 'none'
ripple.style.position = 'absolute'
ripple.style.left = rippleX - radius + 'px'
ripple.style.top = rippleY - radius + 'px'
ripple.style.width = radius * 2 + 'px'
ripple.style.height = radius * 2 + 'px'
ripple.style.borderRadius = '50%'
ripple.style.transformOrigin = '50% 50%'
ripple.style.background = color
ele.append(ripple)
ripple.animate(
[
{ transform: 'scale(0%)' },
{
transform: 'scale(101%)',
},
],
{
duration,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards',
}
)
const onPointerUp = (e: PointerEvent) => {
ele.releasePointerCapture(e.pointerId)
const animation = ripple.animate(
[
{ opacity: '1' },
{
opacity: '0',
},
],
{
duration,
fill: 'forwards',
}
)
animation.oncancel = animation.onfinish = () => {
ripple.remove()
ele.removeEventListener('pointerup', onPointerUp)
ele.removeEventListener('pointercancel', onPointerUp)
}
}
ele.addEventListener('pointerup', onPointerUp)
ele.addEventListener('pointercancel', onPointerUp)
}
ele.addEventListener('pointerdown', onPointerDown)
return () => {
ele.dataset[DatasetName] = 'false'
ele.removeEventListener('pointerdown', onPointerDown)
}
}