mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
refactor[Wave]: CC => FC (#39705)
* fix * refactor[Wave]: CC => FC * fix lint * fix * fix * fix * add test case * add test case * fix test * fix test * test case * add test case * fix * fix * fix * fix * raname * fix * test case * test case * test case * fix test * test case * refactor: Use React way * test: coverage * chore: clean up * rerun fail ci * fix: React 17 error * test: fix test case * test: fix test case * fix borderRadius * test: fix test case * chore: clean up * chore: clean up Co-authored-by: 二货机器人 <smith3816@gmail.com>
This commit is contained in:
parent
365288a9a4
commit
4f16966e28
@ -1,10 +1,8 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import raf from 'rc-util/lib/raf';
|
||||
import React from 'react';
|
||||
import { waitFakeTimer, render, fireEvent } from '../../../tests/utils';
|
||||
import getDataOrAriaProps from '../getDataOrAriaProps';
|
||||
import delayRaf from '../raf';
|
||||
import { isStyleSupport } from '../styleChecker';
|
||||
import throttleByAnimationFrame from '../throttleByAnimationFrame';
|
||||
import TransButton from '../transButton';
|
||||
@ -99,38 +97,6 @@ describe('Test utils function', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('delayRaf', (done) => {
|
||||
jest.useRealTimers();
|
||||
|
||||
let bamboo = false;
|
||||
delayRaf(() => {
|
||||
bamboo = true;
|
||||
}, 3);
|
||||
|
||||
// Do nothing, but insert in the frame
|
||||
// https://github.com/ant-design/ant-design/issues/16290
|
||||
delayRaf(() => {}, 3);
|
||||
|
||||
// Variable bamboo should be false in frame 2 but true in frame 4
|
||||
raf(() => {
|
||||
expect(bamboo).toBe(false);
|
||||
|
||||
// Frame 2
|
||||
raf(() => {
|
||||
expect(bamboo).toBe(false);
|
||||
|
||||
// Frame 3
|
||||
raf(() => {
|
||||
// Frame 4
|
||||
raf(() => {
|
||||
expect(bamboo).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransButton', () => {
|
||||
it('can be focus/blur', () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
|
@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import { render, waitFakeTimer, fireEvent, act } from '../../../tests/utils';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import { render, fireEvent, getByText, waitFakeTimer } from '../../../tests/utils';
|
||||
import Wave from '../wave';
|
||||
import type { InternalWave } from '../wave';
|
||||
|
||||
(global as any).isVisible = true;
|
||||
|
||||
jest.mock('rc-util/lib/Dom/isVisible', () => {
|
||||
const mockFn = () => (global as any).isVisible;
|
||||
return mockFn;
|
||||
});
|
||||
|
||||
describe('Wave component', () => {
|
||||
mountTest(Wave);
|
||||
@ -17,6 +22,7 @@ describe('Wave component', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).isVisible = true;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
@ -28,46 +34,52 @@ describe('Wave component', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function filterStyles(styles: any) {
|
||||
return Array.from<HTMLStyleElement>(styles).filter(
|
||||
(style: HTMLStyleElement) => !style.hasAttribute('data-css-hash'),
|
||||
);
|
||||
function getWaveStyle() {
|
||||
const styleObj: Record<string, string> = {};
|
||||
const { style } = document.querySelector<HTMLElement>('.ant-wave')!;
|
||||
style.cssText.split(';').forEach((kv) => {
|
||||
if (kv.trim()) {
|
||||
const cells = kv.split(':');
|
||||
styleObj[cells[0].trim()] = cells[1].trim();
|
||||
}
|
||||
});
|
||||
|
||||
return styleObj;
|
||||
}
|
||||
|
||||
it('isHidden works', () => {
|
||||
const TEST_NODE_ENV = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = 'development';
|
||||
it('work', async () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button">button</button>
|
||||
</Wave>,
|
||||
);
|
||||
expect(container.querySelector('button')?.className).toBe('');
|
||||
|
||||
container.querySelector('button')?.click();
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeTruthy();
|
||||
|
||||
// Match deadline
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
|
||||
expect(
|
||||
container.querySelector('button')?.hasAttribute('ant-click-animating-without-extra-node'),
|
||||
).toBeFalsy();
|
||||
unmount();
|
||||
process.env.NODE_ENV = TEST_NODE_ENV;
|
||||
});
|
||||
|
||||
it('isHidden is mocked', () => {
|
||||
it('invisible in screen', () => {
|
||||
(global as any).isVisible = false;
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button">button</button>
|
||||
</Wave>,
|
||||
);
|
||||
expect(container.querySelector('button')?.className).toBe('');
|
||||
container.querySelector('button')?.click();
|
||||
expect(
|
||||
container.querySelector('button')?.getAttribute('ant-click-animating-without-extra-node'),
|
||||
).toBe('false');
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wave color is grey', async () => {
|
||||
it('wave color is grey', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button
|
||||
@ -78,17 +90,18 @@ describe('Wave component', () => {
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('button')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('button')?.getRootNode() as HTMLButtonElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(0);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
|
||||
expect(style['--wave-scale']).toBeTruthy();
|
||||
expect(style['--wave-color']).toBeFalsy();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wave color is not grey', async () => {
|
||||
it('wave color is not grey', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button" style={{ borderColor: 'red' }}>
|
||||
@ -96,69 +109,61 @@ describe('Wave component', () => {
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('button')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('button')?.getRootNode() as HTMLButtonElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(1);
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: red;');
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('red');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('read wave color from border-top-color', async () => {
|
||||
it('read wave color from border-top-color', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<div style={{ borderTopColor: 'blue' }}>button</div>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('div')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('div')?.getRootNode() as HTMLDivElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(1);
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: blue;');
|
||||
|
||||
fireEvent.click(getByText(container, 'button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('blue');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('read wave color from background color', async () => {
|
||||
it('read wave color from background color', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<div style={{ backgroundColor: 'green' }}>button</div>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('div')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('div')?.getRootNode() as HTMLDivElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(1);
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: green;');
|
||||
|
||||
fireEvent.click(getByText(container, 'button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('green');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('read wave color from border firstly', async () => {
|
||||
it('read wave color from border firstly', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<div style={{ borderColor: 'yellow', backgroundColor: 'green' }}>button</div>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('div')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('div')?.getRootNode() as HTMLDivElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(1);
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: yellow;');
|
||||
|
||||
fireEvent.click(getByText(container, 'button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('yellow');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hidden element with -leave className', async () => {
|
||||
it('hidden element with -leave className', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button" className="xx-leave">
|
||||
@ -166,66 +171,50 @@ describe('Wave component', () => {
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
container.querySelector('button')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('button')?.getRootNode() as HTMLButtonElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(0);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('ConfigProvider csp', async () => {
|
||||
const { container, unmount } = render(
|
||||
<ConfigProvider csp={{ nonce: 'YourNonceCode' }}>
|
||||
<Wave>
|
||||
<button type="button">button</button>
|
||||
</Wave>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
container.querySelector('button')?.click();
|
||||
await waitFakeTimer();
|
||||
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
|
||||
container.querySelector('button')?.getRootNode() as HTMLButtonElement
|
||||
).getElementsByTagName('style');
|
||||
styles = filterStyles(styles);
|
||||
expect(styles[0].getAttribute('nonce')).toBe('YourNonceCode');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('bindAnimationEvent should return when node is null', () => {
|
||||
const ref = React.createRef<InternalWave>();
|
||||
render(
|
||||
<Wave ref={ref}>
|
||||
it('not show when disabled', () => {
|
||||
const { container } = render(
|
||||
<Wave>
|
||||
<button type="button" disabled>
|
||||
button
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('bindAnimationEvent.onClick should return when children is hidden', () => {
|
||||
const ref = React.createRef<InternalWave>();
|
||||
render(
|
||||
<Wave ref={ref}>
|
||||
it('not show when hidden', () => {
|
||||
(global as any).isVisible = false;
|
||||
|
||||
const { container } = render(
|
||||
<Wave>
|
||||
<button type="button" style={{ display: 'none' }}>
|
||||
button
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('bindAnimationEvent.onClick should return when children is input', () => {
|
||||
const ref = React.createRef<InternalWave>();
|
||||
render(
|
||||
<Wave ref={ref}>
|
||||
it('not show when is input', () => {
|
||||
const { container } = render(
|
||||
<Wave>
|
||||
<input />
|
||||
</Wave>,
|
||||
);
|
||||
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
|
||||
|
||||
fireEvent.click(container.querySelector('input')!);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not throw when click it', () => {
|
||||
@ -243,7 +232,7 @@ describe('Wave component', () => {
|
||||
expect(() => render(<Wave />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('wave color should inferred if border is transparent and background is not', async () => {
|
||||
it('wave color should inferred if border is transparent and background is not', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button" style={{ borderColor: 'transparent', background: 'red' }}>
|
||||
@ -252,17 +241,14 @@ describe('Wave component', () => {
|
||||
</Wave>,
|
||||
);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
await waitFakeTimer();
|
||||
let styles = (container.querySelector('button')!.getRootNode() as any).getElementsByTagName(
|
||||
'style',
|
||||
);
|
||||
styles = filterStyles(styles);
|
||||
expect(styles.length).toBe(1);
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: red;');
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('red');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wave color should inferred if borderTopColor is transparent and borderColor is not', async () => {
|
||||
it('wave color should inferred if borderTopColor is transparent and borderColor is not', () => {
|
||||
const { container, unmount } = render(
|
||||
<Wave>
|
||||
<button type="button" style={{ borderColor: 'red', borderTopColor: 'transparent' }}>
|
||||
@ -270,19 +256,16 @@ describe('Wave component', () => {
|
||||
</button>
|
||||
</Wave>,
|
||||
);
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
await waitFakeTimer();
|
||||
let styles = (container.querySelector('button')!.getRootNode() as any).getElementsByTagName(
|
||||
'style',
|
||||
);
|
||||
styles = filterStyles(styles);
|
||||
|
||||
expect(styles[0].innerHTML).toContain('--antd-wave-shadow-color: red;');
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
|
||||
const style = getWaveStyle();
|
||||
expect(style['--wave-color']).toEqual('red');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('Wave style should append to validate element', () => {
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(
|
||||
<Wave>
|
||||
<div className="bamboo" style={{ borderColor: 'red' }} />
|
||||
@ -295,20 +278,12 @@ describe('Wave component', () => {
|
||||
fakeDoc.appendChild(document.createElement('span'));
|
||||
expect(fakeDoc.childNodes).toHaveLength(2);
|
||||
|
||||
const elem = container.querySelector('.bamboo');
|
||||
const elem = container.querySelector('.bamboo')!;
|
||||
elem.getRootNode = () => fakeDoc;
|
||||
|
||||
if (elem) {
|
||||
elem.getRootNode = () => fakeDoc;
|
||||
// Click should not throw
|
||||
fireEvent.click(elem);
|
||||
|
||||
// Click should not throw
|
||||
fireEvent.click(elem);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(fakeDoc.querySelector('style')).toBeTruthy();
|
||||
}
|
||||
|
||||
jest.useRealTimers();
|
||||
expect(fakeDoc.querySelector('.ant-wave')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -1,38 +0,0 @@
|
||||
import raf from 'rc-util/lib/raf';
|
||||
|
||||
interface RafMap {
|
||||
[id: number]: number;
|
||||
}
|
||||
|
||||
let id: number = 0;
|
||||
const ids: RafMap = {};
|
||||
|
||||
// Support call raf with delay specified frame
|
||||
export default function wrapperRaf(callback: () => void, delayFrames: number = 1): number {
|
||||
const myId: number = id++;
|
||||
let restFrames: number = delayFrames;
|
||||
|
||||
function internalCallback() {
|
||||
restFrames -= 1;
|
||||
|
||||
if (restFrames <= 0) {
|
||||
callback();
|
||||
delete ids[myId];
|
||||
} else {
|
||||
ids[myId] = raf(internalCallback);
|
||||
}
|
||||
}
|
||||
|
||||
ids[myId] = raf(internalCallback);
|
||||
|
||||
return myId;
|
||||
}
|
||||
|
||||
wrapperRaf.cancel = function cancel(pid?: number) {
|
||||
if (pid === undefined) return;
|
||||
|
||||
raf.cancel(ids[pid]);
|
||||
delete ids[pid];
|
||||
};
|
||||
|
||||
wrapperRaf.ids = ids; // export this for test usage
|
105
components/_util/wave/WaveEffect.tsx
Normal file
105
components/_util/wave/WaveEffect.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import * as React from 'react';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import { render, unmount } from 'rc-util/lib/React/render';
|
||||
import classNames from 'classnames';
|
||||
import { getTargetWaveColor, getValidateContainer } from './util';
|
||||
|
||||
export interface WaveEffectProps {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
color: string | null;
|
||||
className: string;
|
||||
scale: number;
|
||||
borderRadius: number[];
|
||||
}
|
||||
|
||||
const WaveEffect: React.FC<WaveEffectProps> = (props) => {
|
||||
const { className, left, top, width, height, color, borderRadius, scale } = props;
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const waveStyle = {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
borderRadius: borderRadius.map((radius) => `${radius}px`).join(' '),
|
||||
'--wave-scale': scale,
|
||||
} as React.CSSProperties & {
|
||||
[name: string]: number | string;
|
||||
};
|
||||
|
||||
if (color) {
|
||||
waveStyle['--wave-color'] = color;
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSMotion
|
||||
visible
|
||||
motionAppear
|
||||
motionName="wave-motion"
|
||||
motionDeadline={5000}
|
||||
onAppearEnd={(_, event) => {
|
||||
if (event.deadline || (event as TransitionEvent).propertyName === 'opacity') {
|
||||
const holder = divRef.current?.parentElement!;
|
||||
unmount(holder).then(() => {
|
||||
holder.parentElement?.removeChild(holder);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{({ className: motionClassName }) => (
|
||||
<div ref={divRef} className={classNames(className, motionClassName)} style={waveStyle} />
|
||||
)}
|
||||
</CSSMotion>
|
||||
);
|
||||
};
|
||||
|
||||
function validateNum(value: number) {
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
export default function showWaveEffect(container: Node, node: HTMLElement, className: string) {
|
||||
const nodeStyle = getComputedStyle(node);
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
|
||||
// Get wave color from target
|
||||
const waveColor = getTargetWaveColor(node);
|
||||
|
||||
// Get border radius
|
||||
const {
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
} = nodeStyle;
|
||||
|
||||
// Do scale calc
|
||||
const { offsetWidth } = node;
|
||||
const scale = validateNum(nodeRect.width / offsetWidth);
|
||||
|
||||
// Create holder
|
||||
const holder = document.createElement('div');
|
||||
getValidateContainer(container).appendChild(holder);
|
||||
|
||||
render(
|
||||
<WaveEffect
|
||||
left={nodeRect.left}
|
||||
top={nodeRect.top}
|
||||
width={nodeRect.width}
|
||||
height={nodeRect.height}
|
||||
color={waveColor}
|
||||
className={className}
|
||||
scale={scale}
|
||||
borderRadius={[
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
].map((radius) => validateNum(parseFloat(radius) * scale))}
|
||||
/>,
|
||||
holder,
|
||||
);
|
||||
}
|
71
components/_util/wave/index.ts
Normal file
71
components/_util/wave/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import classNames from 'classnames';
|
||||
import { composeRef, supportRef } from 'rc-util/lib/ref';
|
||||
import isVisible from 'rc-util/lib/Dom/isVisible';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import type { ConfigConsumerProps } from '../../config-provider';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import { cloneElement } from '../reactNode';
|
||||
import useStyle from './style';
|
||||
import useWave from './useWave';
|
||||
|
||||
export interface WaveProps {
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Wave: React.FC<WaveProps> = (props) => {
|
||||
const { children, disabled } = props;
|
||||
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
|
||||
// ============================== Style ===============================
|
||||
const prefixCls = getPrefixCls('wave');
|
||||
const [, hashId] = useStyle(prefixCls);
|
||||
|
||||
// =============================== Wave ===============================
|
||||
const showWave = useWave(containerRef, classNames(prefixCls, hashId));
|
||||
|
||||
// ============================== Effect ==============================
|
||||
React.useEffect(() => {
|
||||
const node = containerRef.current;
|
||||
if (!node || node.nodeType !== 1 || disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Click handler
|
||||
const onClick = (e: MouseEvent) => {
|
||||
// Fix radio button click twice
|
||||
if (
|
||||
(e.target as HTMLElement).tagName === 'INPUT' ||
|
||||
!isVisible(e.target as HTMLElement) ||
|
||||
// No need wave
|
||||
!node.getAttribute ||
|
||||
node.getAttribute('disabled') ||
|
||||
(node as HTMLInputElement).disabled ||
|
||||
node.className.includes('disabled') ||
|
||||
node.className.includes('-leave')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
showWave();
|
||||
};
|
||||
|
||||
// Bind events
|
||||
node.addEventListener('click', onClick, true);
|
||||
return () => {
|
||||
node.removeEventListener('click', onClick, true);
|
||||
};
|
||||
}, [disabled]);
|
||||
|
||||
// ============================== Render ==============================
|
||||
if (!React.isValidElement(children)) {
|
||||
return (children ?? null) as unknown as React.ReactElement;
|
||||
}
|
||||
|
||||
const ref = supportRef(children) ? composeRef((children as any).ref, containerRef) : containerRef;
|
||||
|
||||
return cloneElement(children, { ref });
|
||||
};
|
||||
|
||||
export default Wave;
|
@ -1,261 +0,0 @@
|
||||
import { updateCSS } from 'rc-util/lib/Dom/dynamicCSS';
|
||||
import { composeRef, supportRef } from 'rc-util/lib/ref';
|
||||
import * as React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ConfigConsumerProps, CSPConfig } from '../../config-provider';
|
||||
import { ConfigConsumer, ConfigContext } from '../../config-provider';
|
||||
import raf from '../raf';
|
||||
import { cloneElement } from '../reactNode';
|
||||
import useStyle from './style';
|
||||
|
||||
let styleForPseudo: HTMLStyleElement | null;
|
||||
|
||||
// Where el is the DOM element you'd like to test for visibility
|
||||
function isHidden(element: HTMLElement) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return false;
|
||||
}
|
||||
return !element || element.offsetParent === null || element.hidden;
|
||||
}
|
||||
|
||||
function getValidateContainer(nodeRoot: Node): Element {
|
||||
if (nodeRoot instanceof Document) {
|
||||
return nodeRoot.body;
|
||||
}
|
||||
|
||||
return Array.from(nodeRoot.childNodes).find(
|
||||
(ele) => ele?.nodeType === Node.ELEMENT_NODE,
|
||||
) as Element;
|
||||
}
|
||||
|
||||
function isNotGrey(color: string) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\d.]*)?\)/);
|
||||
if (match && match[1] && match[2] && match[3]) {
|
||||
return !(match[1] === match[2] && match[2] === match[3]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidWaveColor(color: string) {
|
||||
return (
|
||||
color &&
|
||||
color !== '#fff' &&
|
||||
color !== '#ffffff' &&
|
||||
color !== 'rgb(255, 255, 255)' &&
|
||||
color !== 'rgba(255, 255, 255, 1)' &&
|
||||
isNotGrey(color) &&
|
||||
!/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color
|
||||
color !== 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
function getTargetWaveColor(node: HTMLElement) {
|
||||
const computedStyle = getComputedStyle(node);
|
||||
const borderTopColor = computedStyle.getPropertyValue('border-top-color');
|
||||
const borderColor = computedStyle.getPropertyValue('border-color');
|
||||
const backgroundColor = computedStyle.getPropertyValue('background-color');
|
||||
if (isValidWaveColor(borderTopColor)) {
|
||||
return borderTopColor;
|
||||
}
|
||||
if (isValidWaveColor(borderColor)) {
|
||||
return borderColor;
|
||||
}
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
export interface WaveProps {
|
||||
insertExtraNode?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export class InternalWave extends React.Component<WaveProps> {
|
||||
static contextType = ConfigContext;
|
||||
|
||||
private instance?: {
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
private containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
private extraNode: HTMLDivElement;
|
||||
|
||||
private clickWaveTimeoutId: number;
|
||||
|
||||
private animationStartId: number;
|
||||
|
||||
private animationStart: boolean = false;
|
||||
|
||||
private destroyed: boolean = false;
|
||||
|
||||
private csp?: CSPConfig;
|
||||
|
||||
context: ConfigConsumerProps;
|
||||
|
||||
componentDidMount() {
|
||||
this.destroyed = false;
|
||||
const node = this.containerRef.current as HTMLDivElement;
|
||||
if (!node || node.nodeType !== 1) {
|
||||
return;
|
||||
}
|
||||
this.instance = this.bindAnimationEvent(node);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.instance) {
|
||||
this.instance.cancel();
|
||||
}
|
||||
if (this.clickWaveTimeoutId) {
|
||||
clearTimeout(this.clickWaveTimeoutId);
|
||||
}
|
||||
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
onClick = (node: HTMLElement, waveColor: string) => {
|
||||
const { insertExtraNode, disabled } = this.props;
|
||||
|
||||
if (disabled || !node || isHidden(node) || node.className.includes('-leave')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.extraNode = document.createElement('div');
|
||||
const { extraNode } = this;
|
||||
const { getPrefixCls } = this.context;
|
||||
extraNode.className = `${getPrefixCls('')}-click-animating-node`;
|
||||
const attributeName = this.getAttributeName();
|
||||
node.setAttribute(attributeName, 'true');
|
||||
// Not white or transparent or grey
|
||||
if (isValidWaveColor(waveColor)) {
|
||||
extraNode.style.borderColor = waveColor;
|
||||
|
||||
const nodeRoot = node.getRootNode?.() || node.ownerDocument;
|
||||
const nodeBody = getValidateContainer(nodeRoot) ?? nodeRoot;
|
||||
|
||||
styleForPseudo = updateCSS(
|
||||
`
|
||||
[${getPrefixCls('')}-click-animating-without-extra-node='true']::after, .${getPrefixCls('')}-click-animating-node {
|
||||
--antd-wave-shadow-color: ${waveColor};
|
||||
}`,
|
||||
'antd-wave',
|
||||
{ csp: this.csp, attachTo: nodeBody },
|
||||
);
|
||||
}
|
||||
if (insertExtraNode) {
|
||||
node.appendChild(extraNode);
|
||||
}
|
||||
['transition', 'animation'].forEach((name) => {
|
||||
node.addEventListener(`${name}start`, this.onTransitionStart);
|
||||
node.addEventListener(`${name}end`, this.onTransitionEnd);
|
||||
});
|
||||
};
|
||||
|
||||
onTransitionStart = (e: AnimationEvent) => {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = this.containerRef.current as HTMLDivElement;
|
||||
if (!e || e.target !== node || this.animationStart) {
|
||||
return;
|
||||
}
|
||||
this.resetEffect(node);
|
||||
};
|
||||
|
||||
onTransitionEnd = (e: AnimationEvent) => {
|
||||
if (!e || e.animationName !== 'fadeEffect') {
|
||||
return;
|
||||
}
|
||||
this.resetEffect(e.target as HTMLElement);
|
||||
};
|
||||
|
||||
getAttributeName() {
|
||||
const { getPrefixCls } = this.context;
|
||||
const { insertExtraNode } = this.props;
|
||||
return insertExtraNode
|
||||
? `${getPrefixCls('')}-click-animating`
|
||||
: `${getPrefixCls('')}-click-animating-without-extra-node`;
|
||||
}
|
||||
|
||||
bindAnimationEvent = (node?: HTMLElement) => {
|
||||
if (
|
||||
!node ||
|
||||
!node.getAttribute ||
|
||||
node.getAttribute('disabled') ||
|
||||
node.className.includes('disabled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const onClick = (e: MouseEvent) => {
|
||||
// Fix radio button click twice
|
||||
if ((e.target as HTMLElement).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
this.resetEffect(node);
|
||||
// Get wave color from target
|
||||
const waveColor = getTargetWaveColor(node);
|
||||
this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0);
|
||||
|
||||
raf.cancel(this.animationStartId);
|
||||
this.animationStart = true;
|
||||
|
||||
// Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this.
|
||||
this.animationStartId = raf(() => {
|
||||
this.animationStart = false;
|
||||
}, 10);
|
||||
};
|
||||
node.addEventListener('click', onClick, true);
|
||||
return {
|
||||
cancel: () => {
|
||||
node.removeEventListener('click', onClick, true);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
resetEffect(node: HTMLElement) {
|
||||
if (!node || node === this.extraNode || !(node instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const { insertExtraNode } = this.props;
|
||||
const attributeName = this.getAttributeName();
|
||||
node.setAttribute(attributeName, 'false'); // edge has bug on `removeAttribute` #14466
|
||||
|
||||
if (styleForPseudo) {
|
||||
styleForPseudo.innerHTML = '';
|
||||
}
|
||||
|
||||
if (insertExtraNode && this.extraNode && node.contains(this.extraNode)) {
|
||||
node.removeChild(this.extraNode);
|
||||
}
|
||||
['transition', 'animation'].forEach((name) => {
|
||||
node.removeEventListener(`${name}start`, this.onTransitionStart);
|
||||
node.removeEventListener(`${name}end`, this.onTransitionEnd);
|
||||
});
|
||||
}
|
||||
|
||||
renderWave = ({ csp }: ConfigConsumerProps) => {
|
||||
const { children } = this.props;
|
||||
this.csp = csp;
|
||||
|
||||
if (!React.isValidElement(children)) return children;
|
||||
|
||||
let ref: React.Ref<any> = this.containerRef;
|
||||
if (supportRef(children)) {
|
||||
ref = composeRef((children as any).ref, this.containerRef as any);
|
||||
}
|
||||
|
||||
return cloneElement(children, { ref });
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderWave}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
|
||||
const Wave = forwardRef<InternalWave, WaveProps>((props, ref) => {
|
||||
useStyle();
|
||||
return <InternalWave ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export default Wave;
|
@ -1,86 +1,37 @@
|
||||
import { Keyframes, useStyleRegister } from '@ant-design/cssinjs';
|
||||
import { useContext } from 'react';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import type { AliasToken, GenerateStyle, UseComponentStyleResult } from '../../theme/internal';
|
||||
import { useToken } from '../../theme/internal';
|
||||
import { genComponentStyleHook } from '../../theme/internal';
|
||||
import type { FullToken, GenerateStyle } from '../../theme/internal';
|
||||
|
||||
interface WaveToken extends AliasToken {
|
||||
hashId: string;
|
||||
clickAnimatingNode: string;
|
||||
clickAnimatingTrue: string;
|
||||
clickAnimatingWithoutExtraNodeTrue: string;
|
||||
clickAnimatingWithoutExtraNodeTrueAfter: string;
|
||||
}
|
||||
export interface ComponentToken {}
|
||||
|
||||
export interface WaveToken extends FullToken<'Wave'> {}
|
||||
|
||||
const genWaveStyle: GenerateStyle<WaveToken> = (token) => {
|
||||
const waveEffect = new Keyframes('waveEffect', {
|
||||
'100%': {
|
||||
boxShadow: `0 0 0 6px var(--antd-wave-shadow-color)`,
|
||||
},
|
||||
});
|
||||
const { componentCls, colorPrimary } = token;
|
||||
return {
|
||||
[componentCls]: {
|
||||
position: 'fixed',
|
||||
background: 'transparent',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
color: `var(--wave-color, ${colorPrimary})`,
|
||||
|
||||
const fadeEffect = new Keyframes('fadeEffect', {
|
||||
'100%': {
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
boxShadow: `0 0 0 0 currentcolor`,
|
||||
opacity: 0.2,
|
||||
|
||||
return [
|
||||
{
|
||||
[`${token.clickAnimatingWithoutExtraNodeTrue},
|
||||
${token.clickAnimatingTrue}`]: {
|
||||
'--antd-wave-shadow-color': token.colorPrimary,
|
||||
'--scroll-bar': 0,
|
||||
position: 'relative',
|
||||
},
|
||||
[`${token.clickAnimatingWithoutExtraNodeTrueAfter},
|
||||
& ${token.clickAnimatingNode}`]: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
insetInlineStart: 0,
|
||||
insetInlineEnd: 0,
|
||||
bottom: 0,
|
||||
display: 'block',
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: `0 0 0 0 var(--antd-wave-shadow-color)`,
|
||||
opacity: 0.2,
|
||||
animation: {
|
||||
_skip_check_: true,
|
||||
value: `${fadeEffect.getName(token.hashId)} 2s ${
|
||||
token.motionEaseOutCirc
|
||||
}, ${waveEffect.getName(token.hashId)} 0.4s ${token.motionEaseOutCirc}`,
|
||||
// =================== Motion ===================
|
||||
'&.wave-motion-appear': {
|
||||
transition: [
|
||||
`box-shadow 0.4s ${token.motionEaseOutCirc}`,
|
||||
`opacity 2s ${token.motionEaseOutCirc}`,
|
||||
].join(','),
|
||||
|
||||
'&-active': {
|
||||
boxShadow: `0 0 0 calc(6px * var(--wave-scale)) currentcolor`,
|
||||
opacity: 0,
|
||||
},
|
||||
animationFillMode: 'forwards',
|
||||
content: '""',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
{},
|
||||
waveEffect,
|
||||
fadeEffect,
|
||||
];
|
||||
};
|
||||
|
||||
export default (): UseComponentStyleResult => {
|
||||
const [theme, token, hashId] = useToken();
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
|
||||
const clickAnimatingTrue = `[${rootPrefixCls}-click-animating='true']`;
|
||||
const clickAnimatingWithoutExtraNodeTrue = `[${rootPrefixCls}-click-animating-without-extra-node='true']`;
|
||||
const clickAnimatingNode = `.${rootPrefixCls}-click-animating-node`;
|
||||
|
||||
const waveToken: WaveToken = {
|
||||
...token,
|
||||
hashId,
|
||||
clickAnimatingNode,
|
||||
clickAnimatingTrue,
|
||||
clickAnimatingWithoutExtraNodeTrue,
|
||||
clickAnimatingWithoutExtraNodeTrueAfter: `${clickAnimatingWithoutExtraNodeTrue}::after`,
|
||||
};
|
||||
|
||||
return [
|
||||
useStyleRegister({ theme, token, hashId, path: ['wave'] }, () => [genWaveStyle(waveToken)]),
|
||||
hashId,
|
||||
];
|
||||
};
|
||||
|
||||
export default genComponentStyleHook('Wave', (token) => [genWaveStyle(token)]);
|
||||
|
18
components/_util/wave/useWave.ts
Normal file
18
components/_util/wave/useWave.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import showWaveEffect from './WaveEffect';
|
||||
|
||||
export default function useWave(
|
||||
nodeRef: React.RefObject<HTMLElement>,
|
||||
className: string,
|
||||
): VoidFunction {
|
||||
function showWave() {
|
||||
const node = nodeRef.current!;
|
||||
|
||||
// Skip if not exist doc
|
||||
const container = node.getRootNode?.() || node?.ownerDocument;
|
||||
if (container) {
|
||||
showWaveEffect(container, node, className);
|
||||
}
|
||||
}
|
||||
|
||||
return showWave;
|
||||
}
|
45
components/_util/wave/util.ts
Normal file
45
components/_util/wave/util.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export function getValidateContainer(nodeRoot: Node): Element {
|
||||
if (nodeRoot instanceof Document) {
|
||||
return nodeRoot.body;
|
||||
}
|
||||
|
||||
return Array.from(nodeRoot.childNodes).find(
|
||||
(ele) => ele?.nodeType === Node.ELEMENT_NODE,
|
||||
) as Element;
|
||||
}
|
||||
|
||||
export function isNotGrey(color: string) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\d.]*)?\)/);
|
||||
if (match && match[1] && match[2] && match[3]) {
|
||||
return !(match[1] === match[2] && match[2] === match[3]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isValidWaveColor(color: string) {
|
||||
return (
|
||||
color &&
|
||||
color !== '#fff' &&
|
||||
color !== '#ffffff' &&
|
||||
color !== 'rgb(255, 255, 255)' &&
|
||||
color !== 'rgba(255, 255, 255, 1)' &&
|
||||
isNotGrey(color) &&
|
||||
!/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color
|
||||
color !== 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
export function getTargetWaveColor(node: HTMLElement) {
|
||||
const { borderTopColor, borderColor, backgroundColor } = getComputedStyle(node);
|
||||
if (isValidWaveColor(borderTopColor)) {
|
||||
return borderTopColor;
|
||||
}
|
||||
if (isValidWaveColor(borderColor)) {
|
||||
return borderColor;
|
||||
}
|
||||
if (isValidWaveColor(backgroundColor)) {
|
||||
return backgroundColor;
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,26 +1,11 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import Button from '..';
|
||||
import { fireEvent, render, assertsExist } from '../../../tests/utils';
|
||||
import { fireEvent, render } from '../../../tests/utils';
|
||||
|
||||
// Mock Wave ref
|
||||
let waveInstanceMock: any;
|
||||
jest.mock('../../_util/wave', () => {
|
||||
const Wave: typeof import('../../_util/wave') = jest.requireActual('../../_util/wave');
|
||||
const WaveComponent = Wave.default;
|
||||
|
||||
return {
|
||||
...Wave,
|
||||
__esModule: true,
|
||||
default: (props: import('../../_util/wave').WaveProps) => (
|
||||
<WaveComponent
|
||||
ref={(node) => {
|
||||
waveInstanceMock = node;
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
};
|
||||
jest.mock('rc-util/lib/Dom/isVisible', () => {
|
||||
const mockFn = () => true;
|
||||
return mockFn;
|
||||
});
|
||||
|
||||
describe('click wave effect', () => {
|
||||
@ -31,99 +16,38 @@ describe('click wave effect', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
async function clickButton(wrapper: any) {
|
||||
const element = wrapper.container.firstChild;
|
||||
async function clickButton(container: HTMLElement) {
|
||||
const element = container.firstChild;
|
||||
// https://github.com/testing-library/user-event/issues/833
|
||||
await userEvent.setup({ advanceTimers: jest.advanceTimersByTime }).click(element);
|
||||
fireEvent(element, new Event('transitionstart'));
|
||||
fireEvent(element, new Event('animationend'));
|
||||
await userEvent.setup({ advanceTimers: jest.advanceTimersByTime }).click(element as Element);
|
||||
fireEvent(element!, new Event('transitionstart'));
|
||||
fireEvent(element!, new Event('animationend'));
|
||||
}
|
||||
|
||||
it('should have click wave effect for primary button', async () => {
|
||||
const wrapper = render(<Button type="primary">button</Button>);
|
||||
await clickButton(wrapper);
|
||||
expect(wrapper.container.querySelector('.ant-btn')).toHaveAttribute(
|
||||
'ant-click-animating-without-extra-node',
|
||||
);
|
||||
const { container } = render(<Button type="primary">button</Button>);
|
||||
await clickButton(container);
|
||||
expect(document.querySelector('.ant-wave')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have click wave effect for default button', async () => {
|
||||
const wrapper = render(<Button>button</Button>);
|
||||
await clickButton(wrapper);
|
||||
expect(wrapper.container.querySelector('.ant-btn')).toHaveAttribute(
|
||||
'ant-click-animating-without-extra-node',
|
||||
);
|
||||
const { container } = render(<Button>button</Button>);
|
||||
await clickButton(container);
|
||||
expect(document.querySelector('.ant-wave')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have click wave effect for link type button', async () => {
|
||||
const wrapper = render(<Button type="link">button</Button>);
|
||||
await clickButton(wrapper);
|
||||
expect(wrapper.container.querySelector('.ant-btn')).not.toHaveAttribute(
|
||||
'ant-click-animating-without-extra-node',
|
||||
);
|
||||
const { container } = render(<Button type="link">button</Button>);
|
||||
await clickButton(container);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not have click wave effect for text type button', async () => {
|
||||
const wrapper = render(<Button type="text">button</Button>);
|
||||
await clickButton(wrapper);
|
||||
expect(wrapper.container.querySelector('.ant-btn')).not.toHaveAttribute(
|
||||
'ant-click-animating-without-extra-node',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle transitionstart', async () => {
|
||||
const wrapper = render(<Button type="primary">button</Button>);
|
||||
await clickButton(wrapper);
|
||||
const buttonNode = wrapper.container.querySelector('.ant-btn')!;
|
||||
fireEvent(buttonNode, new Event('transitionstart'));
|
||||
expect(wrapper.container.querySelector('.ant-btn')).toHaveAttribute(
|
||||
'ant-click-animating-without-extra-node',
|
||||
);
|
||||
wrapper.unmount();
|
||||
fireEvent(buttonNode, new Event('transitionstart'));
|
||||
});
|
||||
|
||||
it('should run resetEffect in transitionstart', async () => {
|
||||
const wrapper = render(<Button type="primary">button</Button>);
|
||||
assertsExist(waveInstanceMock);
|
||||
const resetEffect = jest.spyOn(waveInstanceMock, 'resetEffect');
|
||||
await clickButton(wrapper);
|
||||
expect(resetEffect).toHaveBeenCalledTimes(1);
|
||||
await userEvent
|
||||
.setup({ advanceTimers: jest.advanceTimersByTime })
|
||||
.click(wrapper.container.querySelector('.ant-btn')!);
|
||||
expect(resetEffect).toHaveBeenCalledTimes(2);
|
||||
waveInstanceMock.animationStart = false;
|
||||
fireEvent(wrapper.container.querySelector('.ant-btn')!, new Event('transitionstart'));
|
||||
expect(resetEffect).toHaveBeenCalledTimes(3);
|
||||
resetEffect.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle transitionend', async () => {
|
||||
const wrapper = render(<Button type="primary">button</Button>);
|
||||
assertsExist(waveInstanceMock);
|
||||
const resetEffect = jest.spyOn(waveInstanceMock, 'resetEffect');
|
||||
await clickButton(wrapper);
|
||||
expect(resetEffect).toHaveBeenCalledTimes(1);
|
||||
const event = new Event('animationend');
|
||||
Object.assign(event, { animationName: 'fadeEffect' });
|
||||
fireEvent(wrapper.container.querySelector('.ant-btn')!, event);
|
||||
expect(resetEffect).toHaveBeenCalledTimes(2);
|
||||
resetEffect.mockRestore();
|
||||
});
|
||||
|
||||
it('Wave on falsy element', async () => {
|
||||
const { default: Wave } = jest.requireActual('../../_util/wave');
|
||||
let waveInstance: any;
|
||||
render(
|
||||
<Wave
|
||||
ref={(node: any) => {
|
||||
waveInstance = node;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
waveInstance.resetEffect();
|
||||
const { container } = render(<Button type="text">button</Button>);
|
||||
await clickButton(container);
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import type { ConfigConsumerProps } from '..';
|
||||
import ConfigProvider, { ConfigContext } from '..';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import { act, fireEvent, render } from '../../../tests/utils';
|
||||
import { fireEvent, render } from '../../../tests/utils';
|
||||
import Button from '../../button';
|
||||
import Input from '../../input';
|
||||
import Table from '../../table';
|
||||
@ -16,26 +16,6 @@ describe('ConfigProvider', () => {
|
||||
</ConfigProvider>
|
||||
));
|
||||
|
||||
it('Content Security Policy', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const csp = { nonce: 'test-antd' };
|
||||
const { container } = render(
|
||||
<ConfigProvider csp={csp}>
|
||||
<Button />
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector('button')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
const styles = Array.from(document.body.querySelectorAll<HTMLStyleElement>('style'));
|
||||
expect(styles[styles.length - 1].nonce).toEqual(csp.nonce);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('autoInsertSpaceInButton', () => {
|
||||
const text = '确定';
|
||||
const { container } = render(
|
||||
|
@ -3,20 +3,24 @@ import Switch from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { waitFakeTimer, fireEvent, render } from '../../../tests/utils';
|
||||
import { fireEvent, render } from '../../../tests/utils';
|
||||
import { resetWarned } from '../../_util/warning';
|
||||
|
||||
jest.mock('rc-util/lib/Dom/isVisible', () => {
|
||||
const mockFn = () => true;
|
||||
return mockFn;
|
||||
});
|
||||
|
||||
describe('Switch', () => {
|
||||
focusTest(Switch, { refFocus: true });
|
||||
mountTest(Switch);
|
||||
rtlTest(Switch);
|
||||
|
||||
it('should has click wave effect', async () => {
|
||||
it('should has click wave effect', () => {
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Switch />);
|
||||
fireEvent.click(container.querySelector('.ant-switch')!);
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('button')!.getAttribute('ant-click-animating')).toBe('true');
|
||||
expect(document.querySelector('.ant-wave')).toBeTruthy();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
@ -90,7 +90,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
||||
);
|
||||
|
||||
return wrapSSR(
|
||||
<Wave insertExtraNode>
|
||||
<Wave>
|
||||
<RcSwitch
|
||||
{...props}
|
||||
prefixCls={prefixCls}
|
||||
|
@ -48,6 +48,7 @@ import type { ComponentToken as UploadComponentToken } from '../../upload/style'
|
||||
import type { ComponentToken as TourComponentToken } from '../../tour/style';
|
||||
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
|
||||
import type { ComponentToken as AppComponentToken } from '../../app/style';
|
||||
import type { ComponentToken as WaveToken } from '../../_util/wave/style';
|
||||
|
||||
export interface ComponentTokenMap {
|
||||
Affix?: {};
|
||||
@ -112,4 +113,7 @@ export interface ComponentTokenMap {
|
||||
Tour?: TourComponentToken;
|
||||
QRCode?: QRCodeComponentToken;
|
||||
App?: AppComponentToken;
|
||||
|
||||
/** @private Internal TS definition. Do not use. */
|
||||
Wave?: WaveToken;
|
||||
}
|
||||
|
@ -589,7 +589,6 @@ exports[`Tour step support Primary 1`] = `
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
ant-click-animating-without-extra-node="false"
|
||||
class="ant-btn ant-btn-default ant-btn-sm ant-tour-next-btn"
|
||||
type="button"
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user