Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor[Wave]: CC => FC #39705

Merged
merged 42 commits into from Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a8fc4a4
fix
li-jia-nan Dec 21, 2022
1ae8152
refactor[Wave]: CC => FC
li-jia-nan Dec 21, 2022
4a320b6
fix lint
li-jia-nan Dec 21, 2022
61b441a
fix
li-jia-nan Dec 21, 2022
51dbed8
fix
li-jia-nan Dec 21, 2022
22fd1a1
fix
li-jia-nan Dec 23, 2022
eae9512
Merge branch 'master' into Wave
li-jia-nan Dec 23, 2022
1f9d0f6
add test case
li-jia-nan Dec 23, 2022
2dbb432
add test case
li-jia-nan Dec 23, 2022
40b415a
fix test
li-jia-nan Dec 23, 2022
7d1b928
fix test
li-jia-nan Dec 23, 2022
ad07897
test case
li-jia-nan Dec 23, 2022
9598d0e
add test case
li-jia-nan Dec 23, 2022
4cd8f99
fix
li-jia-nan Dec 24, 2022
4ae8098
fix
li-jia-nan Dec 24, 2022
bf3bd7c
fix
li-jia-nan Dec 24, 2022
26acfaf
fix
li-jia-nan Dec 24, 2022
b5cc248
raname
li-jia-nan Dec 24, 2022
b61bc3a
fix
li-jia-nan Dec 25, 2022
a9bfdd8
Merge branch 'master' into Wave
li-jia-nan Dec 25, 2022
b17fa01
Merge branch 'master' into Wave
li-jia-nan Dec 25, 2022
73f93b4
Merge branch 'master' into Wave
li-jia-nan Dec 26, 2022
3109469
Merge branch 'master' into Wave
li-jia-nan Dec 26, 2022
e83ff29
Merge branch 'master' into Wave
li-jia-nan Dec 26, 2022
62cd45c
test case
li-jia-nan Dec 26, 2022
593680a
test case
li-jia-nan Dec 26, 2022
2e921bd
test case
li-jia-nan Dec 26, 2022
3803dd2
fix test
li-jia-nan Dec 27, 2022
931835d
test case
li-jia-nan Dec 27, 2022
ec537bc
refactor: Use React way
zombieJ Dec 28, 2022
5f47f71
test: coverage
zombieJ Dec 28, 2022
b272be7
chore: clean up
zombieJ Dec 28, 2022
9bf3b12
rerun fail ci
li-jia-nan Dec 28, 2022
ad28afb
fix: React 17 error
zombieJ Dec 28, 2022
685f4d4
Merge branch 'Wave' of https://github.com/ant-design/ant-design into …
zombieJ Dec 28, 2022
ee5aadc
test: fix test case
zombieJ Dec 28, 2022
bd0605a
test: fix test case
zombieJ Dec 28, 2022
e435948
fix borderRadius
li-jia-nan Dec 28, 2022
c59b1e1
test: fix test case
zombieJ Dec 28, 2022
5af24a5
Merge branch 'Wave' of https://github.com/ant-design/ant-design into …
zombieJ Dec 28, 2022
cfd44fa
chore: clean up
zombieJ Dec 28, 2022
88f97f4
chore: clean up
zombieJ Dec 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 2 additions & 37 deletions components/_util/__tests__/wave.test.tsx
Expand Up @@ -3,7 +3,6 @@ import mountTest from '../../../tests/shared/mountTest';
import { render, waitFakeTimer, fireEvent, act } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import Wave from '../wave';
import type { InternalWave } from '../wave';

describe('Wave component', () => {
mountTest(Wave);
Expand All @@ -28,7 +27,7 @@ describe('Wave component', () => {
}
});

function filterStyles(styles: any) {
function filterStyles(styles: HTMLCollectionOf<HTMLStyleElement>) {
return Array.from<HTMLStyleElement>(styles).filter(
(style: HTMLStyleElement) => !style.hasAttribute('data-css-hash'),
);
Expand Down Expand Up @@ -187,47 +186,13 @@ describe('Wave component', () => {
container.querySelector('button')?.click();
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
container.querySelector<HTMLButtonElement>('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}>
<button type="button" disabled>
button
</button>
</Wave>,
);
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
});

it('bindAnimationEvent.onClick should return when children is hidden', () => {
const ref = React.createRef<InternalWave>();
render(
<Wave ref={ref}>
<button type="button" style={{ display: 'none' }}>
button
</button>
</Wave>,
);
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
});

it('bindAnimationEvent.onClick should return when children is input', () => {
const ref = React.createRef<InternalWave>();
render(
<Wave ref={ref}>
<input />
</Wave>,
);
expect(ref.current?.bindAnimationEvent()).toBe(undefined);
});

it('should not throw when click it', () => {
expect(() => {
const { container } = render(
Expand Down
218 changes: 98 additions & 120 deletions components/_util/wave/index.tsx
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
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 React, { useContext, useEffect, useRef } from 'react';
import type { ConfigConsumerProps, CSPConfig } from '../../config-provider';
import { ConfigConsumer, ConfigContext } from '../../config-provider';
import raf from '../raf';
Expand Down Expand Up @@ -70,115 +70,82 @@ export interface WaveProps {
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;
const Wave: React.FC<WaveProps> = (props) => {
const { children, insertExtraNode, disabled } = props;

const instanceRef = useRef<{ cancel?: () => void }>({});
const containerRef = useRef<HTMLDivElement>(null);
const extraNode = useRef<HTMLDivElement>();
const clickWaveTimeoutId = useRef<NodeJS.Timer | null>(null);
const animationStartId = useRef<number>();
const animationStart = useRef<boolean>(false);
const destroyed = useRef<boolean>(false);
const cspRef = useRef<CSPConfig>({});

const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);

const attributeName = React.useMemo<string>(
() =>
insertExtraNode
? `${getPrefixCls()}-click-animating`
: `${getPrefixCls()}-click-animating-without-extra-node`,
[insertExtraNode],
);

componentDidMount() {
this.destroyed = false;
const node = this.containerRef.current as HTMLDivElement;
if (!node || node.nodeType !== 1) {
const onTransitionStart = (e: AnimationEvent) => {
if (destroyed.current) {
return;
}
this.instance = this.bindAnimationEvent(node);
}

componentWillUnmount() {
if (this.instance) {
this.instance.cancel();
}
if (this.clickWaveTimeoutId) {
clearTimeout(this.clickWaveTimeoutId);
const node = containerRef.current;
if (!e || e.target !== node || animationStart.current) {
return;
}
resetEffect(node as HTMLDivElement);
};

this.destroyed = true;
}

onClick = (node: HTMLElement, waveColor: string) => {
const { insertExtraNode, disabled } = this.props;
const onTransitionEnd = (e: AnimationEvent) => {
if (!e || e.animationName !== 'fadeEffect') {
return;
}
resetEffect(e.target as HTMLDivElement);
};

const onClick = (node: HTMLDivElement, waveColor: string) => {
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();
extraNode.current = document.createElement('div');

extraNode.current.className = `${getPrefixCls()}-click-animating-node`;

node.setAttribute(attributeName, 'true');
// Not white or transparent or grey
if (isValidWaveColor(waveColor)) {
extraNode.style.borderColor = waveColor;
extraNode.current.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 {
[${getPrefixCls()}-click-animating-without-extra-node='true']::after, .${getPrefixCls()}-click-animating-node {
--antd-wave-shadow-color: ${waveColor};
}`,
'antd-wave',
{ csp: this.csp, attachTo: nodeBody },
{ csp: cspRef.current, attachTo: nodeBody },
);
}
if (insertExtraNode) {
node.appendChild(extraNode);
node.appendChild(extraNode.current);
}
['transition', 'animation'].forEach((name) => {
node.addEventListener(`${name}start`, this.onTransitionStart);
node.addEventListener(`${name}end`, this.onTransitionEnd);
node.addEventListener(`${name}start`, onTransitionStart);
node.addEventListener(`${name}end`, 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) => {
const bindAnimationEvent = (node?: HTMLDivElement) => {
if (
!node ||
!node.getAttribute ||
Expand All @@ -187,75 +154,86 @@ export class InternalWave extends React.Component<WaveProps> {
) {
return;
}
const onClick = (e: MouseEvent) => {
const internalClick = (e: MouseEvent) => {
// Fix radio button click twice
if ((e.target as HTMLElement).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) {
return;
}
this.resetEffect(node);
resetEffect(node);
// Get wave color from target
const waveColor = getTargetWaveColor(node);
this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0);
clickWaveTimeoutId.current = setTimeout(() => {
onClick(node, waveColor);
}, 0);

raf.cancel(this.animationStartId);
this.animationStart = true;
raf.cancel(animationStartId.current);
animationStart.current = true;

// Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this.
this.animationStartId = raf(() => {
this.animationStart = false;
animationStartId.current = raf(() => {
animationStart.current = false;
}, 10);
};
node.addEventListener('click', onClick, true);
node.addEventListener('click', internalClick, true);
return {
cancel: () => {
node.removeEventListener('click', onClick, true);
cancel() {
node.removeEventListener('click', internalClick, true);
},
};
};

resetEffect(node: HTMLElement) {
if (!node || node === this.extraNode || !(node instanceof Element)) {
function resetEffect(node: HTMLDivElement) {
if (!node || node === extraNode.current || !(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);
if (insertExtraNode && extraNode.current && node.contains(extraNode.current)) {
node.removeChild(extraNode.current);
}
['transition', 'animation'].forEach((name) => {
node.removeEventListener(`${name}start`, this.onTransitionStart);
node.removeEventListener(`${name}end`, this.onTransitionEnd);
node.removeEventListener(`${name}start`, onTransitionStart);
node.removeEventListener(`${name}end`, 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);
useEffect(() => {
destroyed.current = false;
const node = containerRef.current;
if (!node || node.nodeType !== 1) {
return;
}

return cloneElement(children, { ref });
};

render() {
return <ConfigConsumer>{this.renderWave}</ConfigConsumer>;
}
}

const Wave = forwardRef<InternalWave, WaveProps>((props, ref) => {
instanceRef.current = bindAnimationEvent(node)!;
return () => {
destroyed.current = true;
if (instanceRef.current) {
instanceRef.current.cancel?.();
}
if (clickWaveTimeoutId.current) {
clearTimeout(clickWaveTimeoutId.current);
}
};
}, []);
useStyle();
return <InternalWave ref={ref} {...props} />;
});
return (
<ConfigConsumer>
li-jia-nan marked this conversation as resolved.
Show resolved Hide resolved
{({ csp }: ConfigConsumerProps) => {
cspRef.current = csp!;
if (!React.isValidElement(children)) {
return children;
}
const ref = supportRef(children)
? composeRef((children as any).ref, containerRef)
: containerRef;
return cloneElement(children, { ref });
}}
</ConfigConsumer>
);
};

export default Wave;