feat: wave effect config (#43784)

* feat: support wave effect

* docs: more info

* test: add test case

* docs: update demo

* test: coverage
This commit is contained in:
二货爱吃白萝卜 2023-07-25 16:29:47 +08:00 committed by GitHub
parent bc05a48cf5
commit 8b19102b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 251 additions and 12 deletions

View File

@ -4,6 +4,7 @@ import { render, unmount } from 'rc-util/lib/React/render';
import raf from 'rc-util/lib/raf';
import * as React from 'react';
import { getTargetWaveColor } from './util';
import type { ShowWaveEffect } from './useWave';
function validateNum(value: number) {
return Number.isNaN(value) ? 0 : value;
@ -125,7 +126,7 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
);
};
export default function showWaveEffect(node: HTMLElement, className: string) {
const showWaveEffect: ShowWaveEffect = (node, { className }) => {
// Create holder
const holder = document.createElement('div');
holder.style.position = 'absolute';
@ -134,4 +135,6 @@ export default function showWaveEffect(node: HTMLElement, className: string) {
node?.insertBefore(holder, node?.firstChild);
render(<WaveEffect target={node} className={className} />, holder);
}
};
export default showWaveEffect;

View File

@ -11,10 +11,11 @@ import useWave from './useWave';
export interface WaveProps {
disabled?: boolean;
children?: React.ReactNode;
component?: string;
}
const Wave: React.FC<WaveProps> = (props) => {
const { children, disabled } = props;
const { children, disabled, component } = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const containerRef = useRef<HTMLElement>(null);
@ -23,7 +24,7 @@ const Wave: React.FC<WaveProps> = (props) => {
const [, hashId] = useStyle(prefixCls);
// =============================== Wave ===============================
const showWave = useWave(containerRef, classNames(prefixCls, hashId));
const showWave = useWave(containerRef, classNames(prefixCls, hashId), component);
// ============================== Effect ==============================
React.useEffect(() => {
@ -48,7 +49,7 @@ const Wave: React.FC<WaveProps> = (props) => {
return;
}
showWave();
showWave(e);
};
// Bind events

View File

@ -1,14 +1,40 @@
import * as React from 'react';
import useEvent from 'rc-util/lib/hooks/useEvent';
import showWaveEffect from './WaveEffect';
import { ConfigContext } from '../../config-provider';
import useToken from '../../theme/useToken';
import type { GlobalToken } from '../../theme';
export type ShowWaveEffect = (
element: HTMLElement,
info: {
className: string;
token: GlobalToken;
component?: string;
event: MouseEvent;
},
) => void;
export default function useWave(
nodeRef: React.RefObject<HTMLElement>,
className: string,
): VoidFunction {
function showWave() {
component?: string,
) {
const { wave } = React.useContext(ConfigContext);
const [, token] = useToken();
const showWave = useEvent((event: MouseEvent) => {
const node = nodeRef.current!;
showWaveEffect(node, className);
}
if (wave?.disabled || !node) {
return;
}
const { showEffect } = wave || {};
// Customize wave effect
(showEffect || showWaveEffect)(node, { className, token, component, event });
});
return showWave;
}

View File

@ -289,7 +289,11 @@ const InternalButton: React.ForwardRefRenderFunction<
);
if (!isUnBorderedButtonType(type)) {
buttonNode = <Wave disabled={!!innerLoading}>{buttonNode}</Wave>;
buttonNode = (
<Wave component="Button" disabled={!!innerLoading}>
{buttonNode}
</Wave>
);
}
return wrapSSR(buttonNode);

View File

@ -0,0 +1,50 @@
import React from 'react';
import ConfigProvider from '..';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
import Button from '../../button';
jest.mock('rc-util/lib/Dom/isVisible', () => () => true);
describe('ConfigProvider.Wave', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('disable', async () => {
const showEffect = jest.fn();
const onClick = jest.fn();
const { container } = render(
<ConfigProvider wave={{ disabled: true, showEffect }}>
<Button onClick={onClick} />
</ConfigProvider>,
);
fireEvent.click(container.querySelector('button')!);
await waitFakeTimer();
expect(onClick).toHaveBeenCalled();
expect(showEffect).not.toHaveBeenCalled();
});
it('support customize effect', async () => {
const showEffect = jest.fn();
const onClick = jest.fn();
const { container } = render(
<ConfigProvider wave={{ showEffect }}>
<Button onClick={onClick} />
</ConfigProvider>,
);
fireEvent.click(container.querySelector('button')!);
await waitFakeTimer();
expect(onClick).toHaveBeenCalled();
expect(showEffect).toHaveBeenCalled();
});
});

View File

@ -11,6 +11,7 @@ import type { SpaceProps } from '../space';
import type { AliasToken, MapToken, OverrideToken, SeedToken } from '../theme/interface';
import type { SizeType } from './SizeContext';
import type { RenderEmptyHandler } from './defaultRenderEmpty';
import type { ShowWaveEffect } from '../_util/wave/useWave';
export const defaultIconPrefixCls = 'anticon';
@ -56,6 +57,11 @@ export interface ButtonConfig extends ComponentStyleConfig {
export type PopupOverflow = 'viewport' | 'scroll';
export interface WaveConfig {
disabled?: boolean;
showEffect?: ShowWaveEffect;
}
export interface ConfigConsumerProps {
getTargetContainer?: () => HTMLElement;
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
@ -142,6 +148,8 @@ export interface ConfigConsumerProps {
tree?: ComponentStyleConfig;
colorPicker?: ComponentStyleConfig;
datePicker?: ComponentStyleConfig;
wave?: WaveConfig;
}
const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {

View File

@ -0,0 +1,7 @@
## zh-CN
波纹效果带来了灵动性,可以通过 `component` 判断来自哪个组件。
## en-US
Wave effect brings dynamic. Use `component` to determine which component use it.

View File

@ -0,0 +1,128 @@
import { Button, ConfigProvider, Space } from 'antd';
import React from 'react';
type WaveConfig = NonNullable<Parameters<typeof ConfigProvider>[0]['wave']>;
// Prepare effect holder
const createHolder = (node: HTMLElement) => {
const { borderWidth } = getComputedStyle(node);
const borderWidthNum = parseInt(borderWidth, 10);
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.inset = `-${borderWidthNum}px`;
div.style.borderRadius = 'inherit';
div.style.background = 'transparent';
div.style.zIndex = '999';
div.style.pointerEvents = 'none';
div.style.overflow = 'hidden';
node.appendChild(div);
return div;
};
const createDot = (
holder: HTMLElement,
color: string,
left: number,
top: number,
size: number = 0,
) => {
const dot = document.createElement('div');
dot.style.position = 'absolute';
dot.style.left = `${left}px`;
dot.style.top = `${top}px`;
dot.style.width = `${size}px`;
dot.style.height = `${size}px`;
dot.style.borderRadius = '50%';
dot.style.background = color;
dot.style.transform = 'translate(-50%, -50%)';
dot.style.transition = `all 1s ease-out`;
holder.appendChild(dot);
return dot;
};
// Inset Effect
const showInsetEffect: WaveConfig['showEffect'] = (node, { event, component }) => {
if (component !== 'Button') {
return;
}
const holder = createHolder(node);
const rect = holder.getBoundingClientRect();
const left = event.clientX - rect.left;
const top = event.clientY - rect.top;
const dot = createDot(holder, 'rgba(255, 255, 255, 0.65)', left, top);
// Motion
requestAnimationFrame(() => {
dot.ontransitionend = () => {
holder.parentElement?.removeChild(holder);
};
dot.style.width = '200px';
dot.style.height = '200px';
dot.style.opacity = '0';
});
};
// Shake Effect
const showShakeEffect: WaveConfig['showEffect'] = (node, { component }) => {
if (component !== 'Button') {
return;
}
const seq = [0, -15, 15, -5, 5, 0];
const itv = 10;
let steps = 0;
function loop() {
cancelAnimationFrame((node as any).effectTimeout);
(node as any).effectTimeout = requestAnimationFrame(() => {
const currentStep = Math.floor(steps / itv);
const current = seq[currentStep];
const next = seq[currentStep + 1];
if (!next) {
node.style.transform = '';
node.style.transition = '';
return;
}
// Trans from current to next by itv
const angle = current + ((next - current) / itv) * (steps % itv);
node.style.transform = `rotate(${angle}deg)`;
node.style.transition = 'none';
steps += 1;
loop();
});
}
loop();
};
// Component
const Wrapper = ({ name, ...wave }: WaveConfig & { name: string }) => (
<ConfigProvider wave={wave}>
<Button type="primary">{name}</Button>
</ConfigProvider>
);
const App = () => (
<Space style={{ padding: 24 }} size="large">
<Wrapper name="Disabled" disabled />
<Wrapper name="Default" />
<Wrapper name="Inset" showEffect={showInsetEffect} />
<Wrapper name="Shake" showEffect={showShakeEffect} />
</Space>
);
export default App;

View File

@ -43,6 +43,7 @@ Some components use dynamic style to support wave effect. You can config `csp` p
<code src="./demo/direction.tsx">Direction</code>
<code src="./demo/size.tsx">Component size</code>
<code src="./demo/theme.tsx">Theme</code>
<code src="./demo/wave.tsx">Custom Wave</code>
<code src="./demo/prefixCls.tsx" debug>prefixCls</code>
<code src="./demo/useConfig.tsx" debug>useConfig</code>
@ -152,6 +153,7 @@ const {
| tree | Set Tree common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| typography | Set Typography common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| upload | Set Upload common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| wave | Config wave effect | { disabled?: boolean, showEffect?: (node: HTMLElement, info: { className, token, component }) => void } | - | 5.8.0 |
## FAQ

View File

@ -30,6 +30,7 @@ import type {
PopupOverflow,
Theme,
ThemeConfig,
WaveConfig,
} from './context';
import { ConfigConsumer, ConfigContext, defaultIconPrefixCls } from './context';
import { registerTheme } from './cssVariables';
@ -186,6 +187,11 @@ export interface ConfigProviderProps {
tree?: ComponentStyleConfig;
colorPicker?: ComponentStyleConfig;
datePicker?: ComponentStyleConfig;
/**
* Wave is special component which only patch on the effect of component interaction.
*/
wave?: WaveConfig;
}
interface ProviderChildrenProps extends ConfigProviderProps {
@ -322,6 +328,7 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = (props) => {
tree,
colorPicker,
datePicker,
wave,
} = props;
// =================================== Warning ===================================
@ -420,6 +427,7 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = (props) => {
tree,
colorPicker,
datePicker,
wave,
};
const config = {

View File

@ -44,6 +44,7 @@ export default Demo;
<code src="./demo/direction.tsx">方向</code>
<code src="./demo/size.tsx">组件尺寸</code>
<code src="./demo/theme.tsx">主题</code>
<code src="./demo/wave.tsx">自定义波纹</code>
<code src="./demo/prefixCls.tsx" debug>前缀</code>
<code src="./demo/useConfig.tsx" debug>useConfig</code>
@ -154,6 +155,7 @@ const {
| tree | 设置 Tree 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| typography | 设置 Typography 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| upload | 设置 Upload 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| wave | 设置水波纹特效 | { disabled?: boolean, showEffect?: (node: HTMLElement, info: { className, token, component }) => void } | - | 5.8.0 |
## FAQ

View File

@ -97,7 +97,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((props, ref) =>
const mergedStyle: React.CSSProperties = { ...SWITCH?.style, ...style };
return wrapSSR(
<Wave>
<Wave component="Switch">
<RcSwitch
{...restProps}
prefixCls={prefixCls}

View File

@ -143,7 +143,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
</span>
);
return wrapSSR(isNeedWave ? <Wave>{tagNode}</Wave> : tagNode);
return wrapSSR(isNeedWave ? <Wave component="Tag">{tagNode}</Wave> : tagNode);
};
const Tag = React.forwardRef<HTMLSpanElement, TagProps>(InternalTag) as TagType;