1
This commit is contained in:
+10
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
import { defaultPrefixCls } from '../../config-provider';
|
||||
export const TARGET_CLS = `${defaultPrefixCls}-wave-target`;
|
||||
+7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user