chore: auto merge branches (#39942)

chore: Feature merge master
This commit is contained in:
github-actions[bot] 2022-12-31 15:08:42 +00:00 committed by GitHub
commit ce0271a584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
470 changed files with 1916 additions and 1343 deletions

View File

@ -59,7 +59,7 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] =>
}, {});
const childItems = [];
childItems.push(
...childrenGroup.default.map((item) => ({
...(childrenGroup.default?.map((item) => ({
label: (
<Link to={`${item.link}${search}`}>
{before}
@ -68,7 +68,7 @@ const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] =>
</Link>
),
key: item.link.replace(/(-cn$)/g, ''),
})),
})) ?? []),
);
Object.entries(childrenGroup).forEach(([type, children]) => {
if (type !== 'default') {

2
.dumi/loading.ts Normal file
View File

@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './theme/common/Loading';

View File

@ -1,4 +0,0 @@
/* eslint-disable import/prefer-default-export */
export function isObject(target: any) {
return Object.prototype.toString.call(target) === '[object Object]';
}

View File

@ -1,37 +1,40 @@
import React, { useCallback, useEffect, useState } from 'react';
import { enUS, zhCN, ThemeEditor } from 'antd-token-previewer';
import { Button, ConfigProvider, message, Modal, Typography } from 'antd';
import React, { useCallback, useEffect, useState, Suspense, useLayoutEffect } from 'react';
import { enUS, ThemeEditor, zhCN } from 'antd-token-previewer';
import { Button, ConfigProvider, message, Modal, Spin, Typography } from 'antd';
import type { ThemeConfig } from 'antd/es/config-provider/context';
import { Helmet } from 'dumi';
import { css } from '@emotion/react';
import { EditOutlined } from '@ant-design/icons';
import type { JSONContent, TextContent } from 'vanilla-jsoneditor';
import useLocale from '../../hooks/useLocale';
import JSONEditor from './components/JSONEditor';
import { isObject } from './components/utils';
const JSONEditor = React.lazy(() => import('../../theme/common/JSONEditor'));
function isObject(target: any) {
return Object.prototype.toString.call(target) === '[object Object]';
}
const locales = {
cn: {
title: '主题编辑器',
save: '保存',
reset: '重置',
edit: '代码',
edit: '编辑',
export: '导出',
editModelTitle: '编辑主题配置',
editTitle: '在下方编辑你的主题 JSON 即可',
editJsonContentTypeError: '主题 JSON 格式错误',
editSuccessfully: '编辑成功',
saveSuccessfully: '保存成功',
initialEditor: '正在初始化编辑器...',
},
en: {
title: 'Theme Editor',
save: 'Save',
reset: 'Reset',
edit: 'Code',
edit: 'Edit',
export: 'Export',
editModelTitle: 'edit Theme Config',
editTitle: 'Edit your theme JSON below',
editJsonContentTypeError: 'The theme of the JSON format is incorrect',
editSuccessfully: 'Edited successfully',
saveSuccessfully: 'Saved successfully',
initialEditor: 'Initializing Editor...',
},
};
@ -61,7 +64,7 @@ const CustomTheme = () => {
json: undefined,
});
useEffect(() => {
useLayoutEffect(() => {
const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME);
if (storedConfig) {
setTheme(() => JSON.parse(storedConfig));
@ -83,10 +86,6 @@ const CustomTheme = () => {
messageApi.success(locale.saveSuccessfully);
};
const handleReset = () => {
setTheme({});
};
const handleEditConfig = () => {
setEditModelOpen(true);
};
@ -124,6 +123,22 @@ const CustomTheme = () => {
messageApi.success(locale.editSuccessfully);
}, [themeConfigContent]);
const handleExport = () => {
const file = new File([JSON.stringify(theme, null, 2)], `Ant Design Theme.json`, {
type: 'text/json; charset=utf-8;',
});
const tmpLink = document.createElement('a');
const objectUrl = URL.createObjectURL(file);
tmpLink.href = objectUrl;
tmpLink.download = file.name;
document.body.appendChild(tmpLink);
tmpLink.click();
document.body.removeChild(tmpLink);
URL.revokeObjectURL(objectUrl);
};
return (
<div>
<Helmet>
@ -145,20 +160,25 @@ const CustomTheme = () => {
onOk={editSave}
onCancel={editModelClose}
>
<div>
<div style={{ color: 'rgba(0,0,0,0.65)' }}>{locale.editTitle}</div>
<Suspense
fallback={
<div style={{ textAlign: 'center', width: '100%', padding: '24px 0' }}>
<Spin tip={locale.initialEditor} />
</div>
}
>
<JSONEditor
content={themeConfigContent}
onChange={handleEditConfigChange}
mainMenuBar={false}
/>
</div>
</Suspense>
</Modal>
<Button onClick={handleEditConfig} icon={<EditOutlined />} style={{ marginRight: 8 }}>
{locale.edit}
<Button onClick={handleExport} style={{ marginRight: 8 }}>
{locale.export}
</Button>
<Button onClick={handleReset} style={{ marginRight: 8 }}>
{locale.reset}
<Button onClick={handleEditConfig} style={{ marginRight: 8 }}>
{locale.edit}
</Button>
<Button type="primary" onClick={handleSave}>
{locale.save}

View File

@ -25,7 +25,7 @@ const ColorPicker: React.FC<ColorPickerProps> = (props) => {
const [displayColorPicker, setDisplayColorPicker] = useState<boolean>(false);
const handleClick = () => {
setDisplayColorPicker(displayColorPicker);
setDisplayColorPicker((prev) => !prev);
};
const handleClose = () => {

View File

@ -24,7 +24,6 @@ export default () => {
display: inline-block;
width: 100%;
margin: 0 0 16px;
overflow: hidden;
border: 1px solid ${token.colorSplit};
border-radius: ${token.borderRadius}px;
transition: all 0.2s;
@ -40,6 +39,7 @@ export default () => {
&,
.code-box-demo {
background-color: ${token.colorBgContainer};
border-radius: ${token.borderRadius}px;
&[data-compact] {
padding: 0;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { startTransition, useCallback, useEffect, useMemo } from 'react';
import { createSearchParams, useOutlet, useSearchParams } from 'dumi';
import { ConfigProvider, theme as antdTheme } from 'antd';
import { createCache, StyleProvider } from '@ant-design/cssinjs';
@ -78,10 +78,13 @@ const GlobalLayout: React.FC = () => {
useEffect(() => {
const _theme = searchParams.getAll('theme') as ThemeName[];
const _direction = searchParams.get('direction') as DirectionType;
setSiteState({ theme: _theme, direction: _direction === 'rtl' ? 'rtl' : 'ltr' });
// Handle isMobile
updateMobileMode();
startTransition(() => {
setSiteState({ theme: _theme, direction: _direction === 'rtl' ? 'rtl' : 'ltr' });
// Handle isMobile
updateMobileMode();
});
window.addEventListener('resize', updateMobileMode);
return () => {
window.removeEventListener('resize', updateMobileMode);

2
.gitignore vendored
View File

@ -47,7 +47,7 @@ server/
# Docs templates
scripts/previewEditor/index.html
components/version/version.tsx
components/version/version.ts
components/version/token.json
components/version/token-meta.json
.dumi/tmp

View File

@ -4,6 +4,7 @@ const compileModules = [
'react-dnd-html5-backend',
'@react-dnd',
'dnd-core',
'react-sticky-box',
'tween-one',
'@babel',
'@ant-design',

View File

@ -5,7 +5,7 @@ toc: false
timeline: true
---
`antd` strictly follows [Semantic Versioning 2.0.0](http://semver.org/).
`antd` follows [Semantic Versioning 2.0.0](http://semver.org/).
#### Release Schedule
@ -15,6 +15,25 @@ timeline: true
---
## 5.1.2
`2022-12-30`
- 🆕 Theme Editor supports uploading themes. [#39621](https://github.com/ant-design/ant-design/pull/39621) [@BoyYangzai](https://github.com/BoyYangzai)
- 💄 Refactor wave effect that can now trigger multiple times. [#39705](https://github.com/ant-design/ant-design/pull/39705) [@li-jia-nan](https://github.com/li-jia-nan)
- Table
- 🐞 Fix Table `column.filtered` cannot be updated. [#39883](https://github.com/ant-design/ant-design/pull/39883)
- 🐞 Fix Table fixed column which is sorted or filtered transparent background bug. [#39012](https://github.com/ant-design/ant-design/pull/39012) [@kiner-tang](https://github.com/kiner-tang)
- 🐞 Fix Image preview style conflict with TailwindCSS. [#39914](https://github.com/ant-design/ant-design/pull/39914)
- 🐞 Fix Dropdown `danger` and `disabled` style priority bug. [#39904](https://github.com/ant-design/ant-design/pull/39904) [@Wxh16144](https://github.com/Wxh16144)
- 🐞 Fix App.useApp `modal` default `okText`. [#39884](https://github.com/ant-design/ant-design/pull/39884) [@BoyYangzai](https://github.com/BoyYangzai)
- 💄 Fix Input.Group misplace style when zoom up in windows. [#39842](https://github.com/ant-design/ant-design/pull/39842) [@heiyu4585](https://github.com/heiyu4585)
- 🐞 Fix Slider missing Tooltip appear motion. [#39857](https://github.com/ant-design/ant-design/pull/39857)
- 🐞 Fix QRCode missing expired style. [#39849](https://github.com/ant-design/ant-design/pull/39849) [@li-jia-nan](https://github.com/li-jia-nan)
- 🐞 Fix Tree switcher's background display unexpected in dark theme. [#39838](https://github.com/ant-design/ant-design/pull/39838) [@kiner-tang](https://github.com/kiner-tang)
- 🐞 Fix Menu slide bar style issue when `border` is reset by preset. [#39819](https://github.com/ant-design/ant-design/pull/39819) [@MadCcc](https://github.com/MadCcc)
- 🐞 Fix Checkbox not support Tooltip or Popover when it is `disabled`. [#39829](https://github.com/ant-design/ant-design/pull/39829)
## 5.1.1
`2022-12-26`

View File

@ -5,7 +5,7 @@ toc: false
timeline: true
---
`antd` 严格遵循 [Semantic Versioning 2.0.0](http://semver.org/lang/zh-CN/) 语义化版本规范。
`antd` 遵循 [Semantic Versioning 2.0.0](http://semver.org/lang/zh-CN/) 语义化版本规范。
#### 发布周期
@ -15,6 +15,25 @@ timeline: true
---
## 5.1.2
`2022-12-30`
- 🆕 官网主题编辑器添加主题上传功能。[#39621](https://github.com/ant-design/ant-design/pull/39621) [@BoyYangzai](https://github.com/BoyYangzai)
- 💄 重构水波纹视效,现在可以多个水波纹同时触发了。[#39705](https://github.com/ant-design/ant-design/pull/39705) [@li-jia-nan](https://github.com/li-jia-nan)
- Table
- 🐞 修复 Table `column.filtered` 更新不生效的问题。[#39883](https://github.com/ant-design/ant-design/pull/39883)
- 🐞 修复 Table 排序/筛选的固定列背景色透明的样式异常问题。[#39012](https://github.com/ant-design/ant-design/pull/39012) [@kiner-tang](https://github.com/kiner-tang)
- 🐞 解决 Image 预览样式会被 TailwindCSS 影响的问题。[#39914](https://github.com/ant-design/ant-design/pull/39914)
- 🐞 修复 Dropdown 组件 `danger``disabled` 属性同时使用的样式问题。[#39904](https://github.com/ant-design/ant-design/pull/39904) [@Wxh16144](https://github.com/Wxh16144)
- 🐞 修复 App `useApp``modal` 确认按钮文案。[#39884](https://github.com/ant-design/ant-design/pull/39884) [@BoyYangzai](https://github.com/BoyYangzai)
- 🐞 修复 Input.Group 在 windows 下缩放屏幕时的错位问题。[#39842](https://github.com/ant-design/ant-design/pull/39842) [@heiyu4585](https://github.com/heiyu4585)
- 🐞 修复 Slider 展示 Tooltip 时动画丢失的问题。[#39857](https://github.com/ant-design/ant-design/pull/39857)
- 🐞 修复 QRCode 过期文案在暗色模式下看不清的问题。[#39849](https://github.com/ant-design/ant-design/pull/39849) [@li-jia-nan](https://github.com/li-jia-nan)
- 🐞 修复 Tree 在暗黑模式下 `switcher` 背景显示异常问题。[#39838](https://github.com/ant-design/ant-design/pull/39838) [@kiner-tang](https://github.com/kiner-tang)
- 🐞 修复 Menu 组件滑块在 `border` 被预设值重置时的样式问题。[#39819](https://github.com/ant-design/ant-design/pull/39819)
- 🐞 修复 Checkbox 禁用时不支持 Tooltip 和 Popover 的问题。[#39829](https://github.com/ant-design/ant-design/pull/39829)
## 5.1.1
`2022-12-26`

View File

@ -106,7 +106,7 @@ const App = () => (
- [首页](https://ant.design/)
- [所有组件](https://ant.design/components/overview-cn)
- [Ant Design Pro](http://pro.ant.design/)
- [更新日志](CHANGELOG.en-US.md)
- [更新日志](CHANGELOG.zh-CN.md)
- [React 底层基础组件](http://react-component.github.io/)
- [移动端组件](http://mobile.ant.design)
- [页面级组件](https://procomponents.ant.design)

View File

@ -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>();

View File

@ -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();
});
});

View File

@ -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

View File

@ -5,6 +5,7 @@ export const groupKeysMap = (keys: string[]) => {
});
return map;
};
export const groupDisabledKeysMap = <RecordType extends any[]>(dataSource: RecordType) => {
const map = new Map<string, number>();
dataSource.forEach(({ disabled, key }, index) => {

View File

@ -1,2 +1,2 @@
/** https://github.com/Microsoft/TypeScript/issues/29729 */
export type LiteralUnion<T extends U, U> = T | (U & {});
export type LiteralUnion<T extends string> = T | (string & {});

View 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,
);
}

View 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;

View File

@ -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;

View File

@ -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)]);

View 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;
}

View 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;
}

View File

@ -1,5 +1,4 @@
import classNames from 'classnames';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import * as React from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
@ -317,10 +316,10 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
React.useEffect(() => {
const scrollContainer = getCurrentContainer();
const scrollEvent = addEventListener(scrollContainer, 'scroll', handleScroll);
handleScroll();
scrollContainer?.addEventListener('scroll', handleScroll);
return () => {
scrollEvent?.remove();
scrollContainer?.removeEventListener('scroll', handleScroll);
};
}, [dependencyListItem]);

View File

@ -1,7 +1,6 @@
import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import type { ConfigConsumerProps } from '../config-provider';
@ -36,7 +35,6 @@ const BackTop: React.FC<BackTopProps> = (props) => {
const [visible, setVisible] = React.useState<boolean>(visibilityHeight === 0);
const ref = React.useRef<HTMLDivElement>(null);
const scrollEvent = React.useRef<ReturnType<typeof addEventListener> | null>(null);
const getDefaultTarget = (): HTMLElement | Document | Window =>
ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window;
@ -48,22 +46,18 @@ const BackTop: React.FC<BackTopProps> = (props) => {
},
);
const bindScrollEvent = () => {
const getTarget = target || getDefaultTarget;
const container = getTarget();
scrollEvent.current = addEventListener(container, 'scroll', handleScroll);
handleScroll({ target: container });
};
if (process.env.NODE_ENV !== 'production') {
warning(false, 'BackTop', '`BackTop` is deprecated, please use `FloatButton.BackTop` instead.');
}
React.useEffect(() => {
bindScrollEvent();
const getTarget = target || getDefaultTarget;
const container = getTarget();
handleScroll({ target: container });
container?.addEventListener('scroll', handleScroll);
return () => {
handleScroll.cancel();
scrollEvent.current?.remove();
container?.removeEventListener('scroll', handleScroll);
};
}, [target]);

View File

@ -13,7 +13,7 @@ export interface RibbonProps {
prefixCls?: string;
style?: React.CSSProperties; // style of ribbon element, not the wrapper
text?: React.ReactNode;
color?: LiteralUnion<PresetColorType, string>;
color?: LiteralUnion<PresetColorType>;
children?: React.ReactNode;
placement?: RibbonPlacement;
}

View File

@ -30,7 +30,7 @@ export interface BadgeProps {
scrollNumberPrefixCls?: string;
className?: string;
status?: PresetStatusColorType;
color?: LiteralUnion<PresetColorType, string>;
color?: LiteralUnion<PresetColorType>;
text?: React.ReactNode;
size?: 'default' | 'small';
offset?: [number | string, number | string];

View File

@ -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();
});
});

Some files were not shown because too many files have changed in this diff Show More