Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common
Submodule common updated 182 files
195 changes: 16 additions & 179 deletions packages/components/hooks/useRipple.ts
Original file line number Diff line number Diff line change
@@ -1,190 +1,27 @@
import { useCallback, useEffect, useMemo } from 'react';
import setStyle from '@tdesign/common-js/utils/setStyle';
import { useEffect } from 'react';
import { bindRipple } from '@tdesign/common-js/utils/ripple';

import { canUseDocument } from '../_util/dom';
import useAnimation from './useAnimation';
import useConfig from './useConfig';

const period = 200;
const elementTransitionPeriod = 200;
const rippleExtraWidth = 20;
const rippleSkew = 'skewX(-8deg)';
const noneRippleBg = 'rgba(0, 0, 0, 0)';
const defaultRippleColor = 'rgba(0, 0, 0, 0.35)';

// 设置动画颜色 get the ripple animation color
const getRippleColor = (el: HTMLElement, fixedRippleColor?: string) => {
// get fixed color from params
if (fixedRippleColor) {
return fixedRippleColor;
}
// get dynamic color from the dataset
if (el?.dataset?.ripple) {
const rippleColor = el.dataset.ripple;
return rippleColor;
}
// use css variable
const cssVariable = getComputedStyle(el).getPropertyValue('--ripple-color');
if (cssVariable) {
return cssVariable;
}
return defaultRippleColor;
};
import useLatest from './useLatest';

/**
* 斜八角动画 hooks 支持三种方式使用
* 1. fixedRippleColor 固定色值 useRipple(ref,fixedRippleColor);
* 2. dynamicColor 动态色值 data.ripple="rippleColor" useRipple(ref)
* 3. CSS variables(recommended) 配合节点对应 CSS 设置 --ripple-color useRipple(ref)
* @param dom 需要使用斜八角动画的 DOM
* @param fixedRippleColor 斜八角的动画颜色
* 为节点挂上斜八角按压波纹动画。
* 波纹颜色优先使用参数 `rippleColor`。
* 未传入时依次读取节点的 `data-ripple` 和 CSS 变量 `--ripple-color`。
*/
export default function useRipple(el: HTMLElement, fixedRippleColor?: string): void {
export default function useRipple(el: HTMLElement, rippleColor?: string): void {
const { classPrefix } = useConfig();
// 全局配置
const { keepRipple } = useAnimation();
const rippleContainer = useMemo(() => {
if (!canUseDocument) return null;
const container = document.createElement('div');
container.className = `${classPrefix}-ripple`;

return container;
}, [classPrefix]);

// 为节点添加斜八角动画 add ripple to the DOM and set up the animation
const handleAddRipple = useCallback(
(e) => {
const rippleColor = getRippleColor(el, fixedRippleColor);
if (e.button !== 0 || !el || !keepRipple) return;

if (
el.classList.contains(`${classPrefix}-is-active`) ||
el.classList.contains(`${classPrefix}-is-disabled`) ||
el.classList.contains(`${classPrefix}-is-checked`) ||
el.classList.contains(`${classPrefix}-is-loading`)
)
return;

const elStyle = getComputedStyle(el);

const elBorder = parseInt(elStyle.borderWidth, 10);
const border = elBorder > 0 ? elBorder : 0;

if (rippleContainer.parentNode === null) {
setStyle(rippleContainer, {
position: 'absolute',
left: `${0 - border}px`,
top: `${0 - border}px`,
width: `calc(100% + ${border * 2}px)`,
height: `calc(100% + ${border * 2}px)`,
borderRadius: elStyle.borderRadius,
pointerEvents: 'none',
overflow: 'hidden',
});
el.appendChild(rippleContainer);
}
// 新增一个 ripple
const ripple = document.createElement('div');

ripple.className = `${classPrefix}-ripple__inner`;

setStyle(ripple, {
marginTop: '0',
marginLeft: '0',
right: '100%',
// 使用百分比尺寸与位移,让 ripple 在 loading icon 插入导致按钮变宽时仍覆盖完整按钮
width: `calc(100% + ${rippleExtraWidth}px)`,
height: '100%',
transition: `transform ${period}ms cubic-bezier(.38, 0, .24, 1), background ${period * 2}ms linear`,
transform: rippleSkew,
pointerEvents: 'none',
position: 'absolute',
zIndex: 0,
backgroundColor: rippleColor,
opacity: '0.9',
});

// fix zIndex:避免遮盖内部元素
const elMap = new WeakMap();
for (let n = el.children.length, i = 0; i < n; ++i) {
const child = el.children[i];
if ((child as HTMLElement).style.zIndex === '' && child !== rippleContainer) {
(child as HTMLElement).style.zIndex = '1';
elMap.set(child, true);
}
}

// fix position
const initPosition = el.style.position ? el.style.position : getComputedStyle(el).position;
if (initPosition === '' || initPosition === 'static') {
// eslint-disable-next-line no-param-reassign
el.style.position = 'relative';
}
rippleContainer.insertBefore(ripple, rippleContainer.firstChild);

setTimeout(() => {
ripple.style.transform = `translateX(calc(100% - ${rippleExtraWidth}px)) ${rippleSkew}`;
}, 0);
// 清除动画节点 clear ripple container
let cleared = false;
let classChangeObserver: MutationObserver | null = null;
const handleClearRipple = () => {
if (cleared) return;
cleared = true;

ripple.style.backgroundColor = noneRippleBg;

if (classChangeObserver) {
classChangeObserver.disconnect();
classChangeObserver = null;
}

if (el) {
el.removeEventListener('pointerup', handleClearRipple, false);
el.removeEventListener('pointerleave', handleClearRipple, false);
}

setTimeout(
() => {
ripple.remove();
if (rippleContainer.children.length === 0) rippleContainer.remove();
},
period * 2 + 100,
);
};

if (typeof MutationObserver !== 'undefined') {
classChangeObserver = new MutationObserver(() => {
if (!el || !(el instanceof Element)) return;
const cls = el.classList;
if (
cls.contains(`${classPrefix}-is-loading`) ||
cls.contains(`${classPrefix}-is-disabled`) ||
cls.contains(`${classPrefix}-is-active`) ||
cls.contains(`${classPrefix}-is-checked`)
) {
ripple.style.transition = `transform ${period}ms cubic-bezier(.38, 0, .24, 1), background ${elementTransitionPeriod}ms linear`;
handleClearRipple();
}
});
classChangeObserver.observe(el, {
attributes: true,
attributeFilter: ['class'],
});
}

el.addEventListener('pointerup', handleClearRipple, false);
el.addEventListener('pointerleave', handleClearRipple, false);
},
[classPrefix, el, fixedRippleColor, rippleContainer, keepRipple],
);
const optionsRef = useLatest({
classPrefix,
enable: keepRipple,
rippleColor,
});

useEffect(() => {
if (!el) return;
el.addEventListener('pointerdown', handleAddRipple, false);

return () => {
el.removeEventListener('pointerdown', handleAddRipple, false);
};
}, [handleAddRipple, fixedRippleColor, el]);
if (!el) return undefined;
return bindRipple(el, () => optionsRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [el]);
}
Loading