This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import type { WaveProps } from '.';
import type { ShowWaveEffect } from './interface';
export interface WaveEffectProps {
className: string;
target: HTMLElement;
component?: string;
colorSource?: WaveProps['colorSource'];
}
declare const showWaveEffect: ShowWaveEffect;
export default showWaveEffect;
+137
View File
@@ -0,0 +1,137 @@
"use client";
import * as React from 'react';
import CSSMotion from '@rc-component/motion';
import raf from "@rc-component/util/es/raf";
import { render, unmount } from "@rc-component/util/es/React/render";
import { composeRef } from "@rc-component/util/es/ref";
import { clsx } from 'clsx';
import { ConfigContext } from '../../config-provider';
import { genCssVar } from '../../theme/util/genStyleUtils';
import { TARGET_CLS } from './interface';
import { getTargetWaveColor } from './util';
function validateNum(value) {
return Number.isNaN(value) ? 0 : value;
}
const WaveEffect = props => {
const {
className,
target,
component,
colorSource
} = props;
const divRef = React.useRef(null);
const {
getPrefixCls
} = React.useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
const [varName] = genCssVar(rootPrefixCls, 'wave');
// ===================== Effect =====================
const [waveColor, setWaveColor] = React.useState(null);
const [borderRadius, setBorderRadius] = React.useState([]);
const [left, setLeft] = React.useState(0);
const [top, setTop] = React.useState(0);
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const [enabled, setEnabled] = React.useState(false);
const waveStyle = {
left,
top,
width,
height,
borderRadius: borderRadius.map(radius => `${radius}px`).join(' ')
};
if (waveColor) {
waveStyle[varName('color')] = waveColor;
}
function syncPos() {
const nodeStyle = getComputedStyle(target);
// Get wave color from target
setWaveColor(getTargetWaveColor(target, colorSource));
const isStatic = nodeStyle.position === 'static';
// Rect
const {
borderLeftWidth,
borderTopWidth
} = nodeStyle;
setLeft(isStatic ? target.offsetLeft : validateNum(-Number.parseFloat(borderLeftWidth)));
setTop(isStatic ? target.offsetTop : validateNum(-Number.parseFloat(borderTopWidth)));
setWidth(target.offsetWidth);
setHeight(target.offsetHeight);
// Get border radius
const {
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius
} = nodeStyle;
setBorderRadius([borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius].map(radius => validateNum(Number.parseFloat(radius))));
}
React.useEffect(() => {
if (target) {
// We need delay to check position here
// since UI may change after click
const id = raf(() => {
syncPos();
setEnabled(true);
});
// Add resize observer to follow size
let resizeObserver;
if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(syncPos);
resizeObserver.observe(target);
}
return () => {
raf.cancel(id);
resizeObserver?.disconnect();
};
}
}, [target]);
if (!enabled) {
return null;
}
const isSmallComponent = (component === 'Checkbox' || component === 'Radio') && target?.classList.contains(TARGET_CLS);
return /*#__PURE__*/React.createElement(CSSMotion, {
visible: true,
motionAppear: true,
motionName: "wave-motion",
motionDeadline: 5000,
onAppearEnd: (_, event) => {
if (event.deadline || event.propertyName === 'opacity') {
const holder = divRef.current?.parentElement;
unmount(holder).then(() => {
holder?.remove();
});
}
return false;
}
}, ({
className: motionClassName
}, ref) => (/*#__PURE__*/React.createElement("div", {
ref: composeRef(divRef, ref),
className: clsx(className, motionClassName, {
'wave-quick': isSmallComponent
}),
style: waveStyle
})));
};
const showWaveEffect = (target, info) => {
const {
component
} = info;
// Skip for unchecked checkbox
if (component === 'Checkbox' && !target.querySelector('input')?.checked) {
return;
}
// Create holder
const holder = document.createElement('div');
holder.style.position = 'absolute';
holder.style.left = '0px';
holder.style.top = '0px';
target?.insertBefore(holder, target?.firstChild);
render(/*#__PURE__*/React.createElement(WaveEffect, {
...info,
target: target
}), holder);
};
export default showWaveEffect;
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import type { WaveComponent } from './interface';
export interface WaveProps {
disabled?: boolean;
children?: React.ReactNode;
component?: WaveComponent;
colorSource?: 'color' | 'backgroundColor' | 'borderColor' | null;
}
declare const Wave: React.FC<WaveProps>;
export default Wave;
+59
View File
@@ -0,0 +1,59 @@
import React, { useContext, useRef } from 'react';
import isVisible from "@rc-component/util/es/Dom/isVisible";
import { composeRef, getNodeRef, supportRef } from "@rc-component/util/es/ref";
import { clsx } from 'clsx';
import { ConfigContext } from '../../config-provider';
import { cloneElement } from '../reactNode';
import useStyle from './style';
import useWave from './useWave';
const Wave = props => {
const {
children,
disabled,
component,
colorSource
} = props;
const {
getPrefixCls
} = useContext(ConfigContext);
const containerRef = useRef(null);
// ============================== Style ===============================
const prefixCls = getPrefixCls('wave');
const hashId = useStyle(prefixCls);
// =============================== Wave ===============================
const showWave = useWave(containerRef, clsx(prefixCls, hashId), component, colorSource);
// ============================== Effect ==============================
React.useEffect(() => {
const node = containerRef.current;
if (!node || node.nodeType !== window.Node.ELEMENT_NODE || disabled) {
return;
}
// Click handler
const onClick = e => {
// Fix radio button click twice
if (!isVisible(e.target) ||
// No need wave
!node.getAttribute || node.getAttribute('disabled') || node.disabled || node.className.includes('disabled') && !node.className.includes('disabled:') || node.getAttribute('aria-disabled') === 'true' || node.className.includes('-leave')) {
return;
}
showWave(e);
};
// Bind events
node.addEventListener('click', onClick, true);
return () => {
node.removeEventListener('click', onClick, true);
};
}, [disabled]);
// ============================== Render ==============================
if (! /*#__PURE__*/React.isValidElement(children)) {
return children ?? null;
}
const ref = supportRef(children) ? composeRef(getNodeRef(children), containerRef) : containerRef;
return cloneElement(children, {
ref
});
};
if (process.env.NODE_ENV !== 'production') {
Wave.displayName = 'Wave';
}
export default Wave;
+13
View File
@@ -0,0 +1,13 @@
import type { WaveProps } from '.';
import type { GlobalToken } from '../../theme/internal';
export declare const TARGET_CLS = "ant-wave-target";
export type ShowWaveEffect = (element: HTMLElement, info: {
className: string;
token: GlobalToken;
component?: WaveComponent;
event: MouseEvent;
hashId: string;
colorSource?: WaveProps['colorSource'];
}) => void;
export type ShowWave = (event: MouseEvent) => void;
export type WaveComponent = 'Tag' | 'Button' | 'Checkbox' | 'Radio' | 'Switch' | 'Steps';
+2
View File
@@ -0,0 +1,2 @@
import { defaultPrefixCls } from '../../config-provider';
export const TARGET_CLS = `${defaultPrefixCls}-wave-target`;
+7
View File
@@ -0,0 +1,7 @@
import type { FullToken } from '../../theme/internal';
export interface ComponentToken {
}
export interface WaveToken extends FullToken<'Wave'> {
}
declare const _default: (prefixCls: string, rootCls?: string) => string;
export default _default;
+36
View File
@@ -0,0 +1,36 @@
import { genComponentStyleHook } from '../../theme/internal';
import { genCssVar } from '../../theme/util/genStyleUtils';
const genWaveStyle = token => {
const {
componentCls,
colorPrimary,
motionDurationSlow,
motionEaseInOut,
motionEaseOutCirc,
antCls
} = token;
const [, varRef] = genCssVar(antCls, 'wave');
return {
[componentCls]: {
position: 'absolute',
background: 'transparent',
pointerEvents: 'none',
boxSizing: 'border-box',
color: varRef('color', colorPrimary),
boxShadow: `0 0 0 0 currentcolor`,
opacity: 0.2,
// =================== Motion ===================
'&.wave-motion-appear': {
transition: [`box-shadow 0.4s`, `opacity 2s`].map(prop => `${prop} ${motionEaseOutCirc}`).join(','),
'&-active': {
boxShadow: `0 0 0 6px currentcolor`,
opacity: 0
},
'&.wave-quick': {
transition: [`box-shadow`, `opacity`].map(prop => `${prop} ${motionDurationSlow} ${motionEaseInOut}`).join(',')
}
}
}
};
};
export default genComponentStyleHook('Wave', genWaveStyle);
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import type { WaveProps } from '.';
import type { ShowWave, WaveComponent } from './interface';
declare const useWave: (nodeRef: React.RefObject<HTMLElement | null>, className: string, component?: WaveComponent, colorSource?: WaveProps["colorSource"]) => ShowWave;
export default useWave;
+46
View File
@@ -0,0 +1,46 @@
import * as React from 'react';
import { useEvent } from '@rc-component/util';
import raf from "@rc-component/util/es/raf";
import { ConfigContext } from '../../config-provider';
import useToken from '../../theme/useToken';
import { TARGET_CLS } from './interface';
import showWaveEffect from './WaveEffect';
const useWave = (nodeRef, className, component, colorSource) => {
const {
wave
} = React.useContext(ConfigContext);
const [, token, hashId] = useToken();
const showWave = useEvent(event => {
const node = nodeRef.current;
if (wave?.disabled || !node) {
return;
}
const targetNode = node.querySelector(`.${TARGET_CLS}`) || node;
const {
showEffect
} = wave || {};
// Customize wave effect
(showEffect || showWaveEffect)(targetNode, {
className,
token,
component,
event,
hashId,
colorSource
});
});
const rafIdRef = React.useRef(null);
// Clean up RAF on unmount to prevent memory leaks and stale callbacks
React.useEffect(() => () => {
raf.cancel(rafIdRef.current);
}, []);
// Merge trigger event into one for each frame
const showDebounceWave = event => {
raf.cancel(rafIdRef.current);
rafIdRef.current = raf(() => {
showWave(event);
});
};
return showDebounceWave;
};
export default useWave;
+2
View File
@@ -0,0 +1,2 @@
export declare function isValidWaveColor(color: CSSStyleDeclaration[keyof CSSStyleDeclaration]): color is string;
export declare function getTargetWaveColor(node: HTMLElement, colorSource?: keyof CSSStyleDeclaration | null): string | null;
+17
View File
@@ -0,0 +1,17 @@
export function isValidWaveColor(color) {
return color && typeof color === 'string' && color !== '#fff' && color !== '#ffffff' && color !== 'rgb(255, 255, 255)' && color !== 'rgba(255, 255, 255, 1)' && !/rgba\((?:\d*, ){3}0\)/.test(color) &&
// any transparent rgba color
color !== 'transparent' && color !== 'canvastext';
}
export function getTargetWaveColor(node, colorSource = null) {
const style = getComputedStyle(node);
const {
borderTopColor,
borderColor,
backgroundColor
} = style;
if (colorSource && isValidWaveColor(style[colorSource])) {
return style[colorSource];
}
return [borderTopColor, borderColor, backgroundColor].find(isValidWaveColor) ?? null;
}