diff --git a/.gitignore b/.gitignore index 16c86a873c..eeed7b1d34 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ server/ # Docs templates scripts/previewEditor/index.html components/version/version.ts +components/version/version.tsx components/version/token.json components/version/token-meta.json .dumi/tmp diff --git a/components/_util/__tests__/wave.test.tsx b/components/_util/__tests__/wave.test.tsx index c47fa6055e..b6829855b1 100644 --- a/components/_util/__tests__/wave.test.tsx +++ b/components/_util/__tests__/wave.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import mountTest from '../../../tests/shared/mountTest'; -import { render, fireEvent, getByText, waitFakeTimer } from '../../../tests/utils'; +import { render, fireEvent, getByText, waitFakeTimer, act } from '../../../tests/utils'; import Wave from '../wave'; (global as any).isVisible = true; @@ -13,12 +13,29 @@ jest.mock('rc-util/lib/Dom/isVisible', () => { describe('Wave component', () => { mountTest(Wave); + let obCnt = 0; + let disCnt = 0; + beforeAll(() => { + /* eslint-disable class-methods-use-this */ + class FakeResizeObserver { + observe = () => { + obCnt += 1; + }; + + disconnect = () => { + disCnt += 1; + }; + } + + (window as any).ResizeObserver = FakeResizeObserver; jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); + expect(obCnt).not.toBe(0); + expect(disCnt).not.toBe(0); }); beforeEach(() => { @@ -47,6 +64,12 @@ describe('Wave component', () => { return styleObj; } + function waitRaf() { + act(() => { + jest.advanceTimersByTime(100); + }); + } + it('work', async () => { const { container, unmount } = render( @@ -55,6 +78,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); expect(document.querySelector('.ant-wave')).toBeTruthy(); // Match deadline @@ -74,6 +98,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); expect(document.querySelector('.ant-wave')).toBeFalsy(); unmount(); @@ -92,6 +117,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); const style = getWaveStyle(); @@ -110,6 +136,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('red'); @@ -125,6 +152,7 @@ describe('Wave component', () => { ); fireEvent.click(getByText(container, 'button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('blue'); @@ -140,6 +168,7 @@ describe('Wave component', () => { ); fireEvent.click(getByText(container, 'button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('green'); @@ -155,6 +184,7 @@ describe('Wave component', () => { ); fireEvent.click(getByText(container, 'button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('yellow'); @@ -172,6 +202,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); expect(document.querySelector('.ant-wave')).toBeFalsy(); unmount(); @@ -202,6 +233,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); expect(document.querySelector('.ant-wave')).toBeFalsy(); }); @@ -213,6 +245,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('input')!); + waitRaf(); expect(document.querySelector('.ant-wave')).toBeFalsy(); }); @@ -224,11 +257,15 @@ describe('Wave component', () => { , ); fireEvent.click(container); + waitRaf(); }).not.toThrow(); }); it('should not throw when no children', () => { - expect(() => render()).not.toThrow(); + expect(() => { + render(); + waitRaf(); + }).not.toThrow(); }); it('wave color should inferred if border is transparent and background is not', () => { @@ -240,6 +277,7 @@ describe('Wave component', () => { , ); fireEvent.click(container.querySelector('button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('red'); @@ -257,6 +295,7 @@ describe('Wave component', () => { ); fireEvent.click(container.querySelector('button')!); + waitRaf(); const style = getWaveStyle(); expect(style['--wave-color']).toEqual('red'); @@ -282,6 +321,7 @@ describe('Wave component', () => { // Click should not throw fireEvent.click(elem); + waitRaf(); expect(container.querySelector('.ant-wave')).toBeTruthy(); }); diff --git a/components/_util/wave/WaveEffect.tsx b/components/_util/wave/WaveEffect.tsx index 905c6bb0aa..9bdbb938d6 100644 --- a/components/_util/wave/WaveEffect.tsx +++ b/components/_util/wave/WaveEffect.tsx @@ -1,23 +1,31 @@ import * as React from 'react'; import CSSMotion from 'rc-motion'; +import raf from 'rc-util/lib/raf'; import { render, unmount } from 'rc-util/lib/React/render'; import classNames from 'classnames'; import { getTargetWaveColor } from './util'; +function validateNum(value: number) { + return Number.isNaN(value) ? 0 : value; +} + export interface WaveEffectProps { - left: number; - top: number; - width: number; - height: number; - color: string | null; className: string; - borderRadius: number[]; + target: HTMLElement; } const WaveEffect: React.FC = (props) => { - const { className, left, top, width, height, color, borderRadius } = props; + const { className, target } = props; const divRef = React.useRef(null); + const [color, 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, @@ -32,6 +40,65 @@ const WaveEffect: React.FC = (props) => { waveStyle['--wave-color'] = color; } + function syncPos() { + const nodeStyle = getComputedStyle(target); + + // Get wave color from target + setWaveColor(getTargetWaveColor(target)); + + // Rect + setLeft(target.offsetLeft); + setTop(target.offsetTop); + setWidth(target.offsetWidth); + setHeight(target.offsetHeight); + + // Get border radius + const { + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + } = nodeStyle; + + setBorderRadius( + [ + borderTopLeftRadius, + borderTopRightRadius, + borderBottomRightRadius, + borderBottomLeftRadius, + ].map((radius) => validateNum(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: ResizeObserver; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(syncPos); + + resizeObserver.observe(target); + } + + return () => { + raf.cancel(id); + resizeObserver?.disconnect(); + }; + } + }, []); + + if (!enabled) { + return null; + } + return ( = (props) => { ); }; -function validateNum(value: number) { - return Number.isNaN(value) ? 0 : value; -} - export default function showWaveEffect(node: HTMLElement, className: string) { - const nodeStyle = getComputedStyle(node); - - // Get wave color from target - const waveColor = getTargetWaveColor(node); - - // Get border radius - const { - borderTopLeftRadius, - borderTopRightRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - } = nodeStyle; - // Create holder const holder = document.createElement('div'); holder.style.position = 'absolute'; - holder.style.left = `${node.offsetLeft}px`; - holder.style.top = `${node.offsetTop}px`; + holder.style.left = `0px`; + holder.style.top = `0px`; node.parentElement?.appendChild(holder); - render( - validateNum(parseFloat(radius)))} - />, - holder, - ); + render(, holder); } diff --git a/components/button/__tests__/wave.test.tsx b/components/button/__tests__/wave.test.tsx index 71d76e28ae..91ee5e21d1 100644 --- a/components/button/__tests__/wave.test.tsx +++ b/components/button/__tests__/wave.test.tsx @@ -1,7 +1,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import Button from '..'; -import { fireEvent, render } from '../../../tests/utils'; +import { act, fireEvent, render } from '../../../tests/utils'; jest.mock('rc-util/lib/Dom/isVisible', () => { const mockFn = () => true; @@ -23,6 +23,10 @@ describe('click wave effect', () => { const element = container.firstChild; // https://github.com/testing-library/user-event/issues/833 await userEvent.setup({ advanceTimers: jest.advanceTimersByTime }).click(element as Element); + act(() => { + jest.advanceTimersByTime(100); + }); + fireEvent(element!, new Event('transitionstart')); fireEvent(element!, new Event('animationend')); } diff --git a/components/switch/__tests__/index.test.tsx b/components/switch/__tests__/index.test.tsx index 47c1c476f0..844463ac7c 100644 --- a/components/switch/__tests__/index.test.tsx +++ b/components/switch/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import Switch from '..'; import focusTest from '../../../tests/shared/focusTest'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { fireEvent, render } from '../../../tests/utils'; +import { act, fireEvent, render } from '../../../tests/utils'; import { resetWarned } from '../../_util/warning'; jest.mock('rc-util/lib/Dom/isVisible', () => { @@ -20,6 +20,9 @@ describe('Switch', () => { jest.useFakeTimers(); const { container } = render(); fireEvent.click(container.querySelector('.ant-switch')!); + act(() => { + jest.advanceTimersByTime(100); + }); expect(document.querySelector('.ant-wave')).toBeTruthy(); jest.clearAllTimers(); jest.useRealTimers();