mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-12-04 13:08:41 +08:00
commit
ce0271a584
@ -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
2
.dumi/loading.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './theme/common/Loading';
|
@ -1,4 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function isObject(target: any) {
|
||||
return Object.prototype.toString.call(target) === '[object Object]';
|
||||
}
|
@ -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}
|
||||
|
@ -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 = () => {
|
||||
|
@ -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;
|
||||
|
@ -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
2
.gitignore
vendored
@ -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
|
||||
|
1
.jest.js
1
.jest.js
@ -4,6 +4,7 @@ const compileModules = [
|
||||
'react-dnd-html5-backend',
|
||||
'@react-dnd',
|
||||
'dnd-core',
|
||||
'react-sticky-box',
|
||||
'tween-one',
|
||||
'@babel',
|
||||
'@ant-design',
|
||||
|
@ -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`
|
||||
|
@ -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`
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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) => {
|
||||
|
@ -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 & {});
|
||||
|
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,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]);
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user