mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
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:
parent
bc05a48cf5
commit
8b19102b0d
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
50
components/config-provider/__tests__/wave.test.tsx
Normal file
50
components/config-provider/__tests__/wave.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
@ -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) => {
|
||||
|
7
components/config-provider/demo/wave.md
Normal file
7
components/config-provider/demo/wave.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
波纹效果带来了灵动性,可以通过 `component` 判断来自哪个组件。
|
||||
|
||||
## en-US
|
||||
|
||||
Wave effect brings dynamic. Use `component` to determine which component use it.
|
128
components/config-provider/demo/wave.tsx
Normal file
128
components/config-provider/demo/wave.tsx
Normal 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;
|
@ -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
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user