chore: merge master

This commit is contained in:
zombiej 2022-06-21 19:40:22 +08:00
commit 91245c16ad
57 changed files with 1468 additions and 767 deletions

View File

@ -15,6 +15,17 @@ timeline: true
--- ---
## 4.21.3
`2022-06-17`
- 🐞 Fix Table customize `filterDropdown` with Menu should not break default `selectable`. [#36098](https://github.com/ant-design/ant-design/pull/36098)
- 🐞 Fix Input.Textarea cannot focus after click clear icon in controlled mode. [#34728](https://github.com/ant-design/ant-design/pull/34728) [@Pulset](https://github.com/Pulset)
- TypeScript
- 🤖 Tree.DirectoryTree supports generic DataNode type. [#36092](https://github.com/ant-design/ant-design/pull/36092) [@JaylanChen](https://github.com/JaylanChen)
- 🤖 Export `RefSelectProps` from `Select`. [#34732](https://github.com/ant-design/ant-design/pull/34732) [@chentsulin](https://github.com/chentsulin)
- 🤖 Export `FormRule`, `FormListFieldData`, `FormListOperation` from `Form`. [#34735](https://github.com/ant-design/ant-design/pull/34735) [@chentsulin](https://github.com/chentsulin)
## 4.21.2 ## 4.21.2
`2022-06-15` `2022-06-15`

View File

@ -15,6 +15,17 @@ timeline: true
--- ---
## 4.21.3
`2022-06-17`
- 🐞 修复 Table 自定义 `filterDropdown` 中使用 Menu 会被修改默认 `selectable` 的问题。[#36098](https://github.com/ant-design/ant-design/pull/36098)
- 🐞 修复 Input.Textarea 受控时点击清除图标后无法聚焦的问题。[#34728](https://github.com/ant-design/ant-design/pull/34728) [@Pulset](https://github.com/Pulset)
- TypeScript
- 🤖 修复 Tree.DirectoryTree 不支持泛型的问题。[#36092](https://github.com/ant-design/ant-design/pull/36092) [@JaylanChen](https://github.com/JaylanChen)
- 🤖 从 `Select` 导出类型 `RefSelectProps`。[#34732](https://github.com/ant-design/ant-design/pull/34732) [@chentsulin](https://github.com/chentsulin)
- 🤖 从 Form 导出类型 `FormRule`、`FormListFieldData`、`FormListOperation`。[#34735](https://github.com/ant-design/ant-design/pull/34735) [@chentsulin](https://github.com/chentsulin)
## 4.21.2 ## 4.21.2
`2022-06-15` `2022-06-15`

View File

@ -1,15 +1,15 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import warning from '../_util/warning'; import useBreakpoint from '../grid/hooks/useBreakpoint';
import type { Breakpoint } from '../_util/responsiveObserve'; import type { Breakpoint } from '../_util/responsiveObserve';
import { responsiveArray } from '../_util/responsiveObserve'; import { responsiveArray } from '../_util/responsiveObserve';
import useBreakpoint from '../grid/hooks/useBreakpoint'; import warning from '../_util/warning';
import useStyle from './style';
import type { AvatarSize } from './SizeContext'; import type { AvatarSize } from './SizeContext';
import SizeContext from './SizeContext'; import SizeContext from './SizeContext';
import useStyle from './style';
export interface AvatarProps { export interface AvatarProps {
/** Shape of avatar, options: `circle`, `square` */ /** Shape of avatar, options: `circle`, `square` */
@ -241,7 +241,9 @@ const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (pr
}; };
const Avatar = React.forwardRef<unknown, AvatarProps>(InternalAvatar); const Avatar = React.forwardRef<unknown, AvatarProps>(InternalAvatar);
Avatar.displayName = 'Avatar'; if (process.env.NODE_ENV !== 'production') {
Avatar.displayName = 'Avatar';
}
Avatar.defaultProps = { Avatar.defaultProps = {
shape: 'circle' as AvatarProps['shape'], shape: 'circle' as AvatarProps['shape'],

View File

@ -318,8 +318,9 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
}; };
const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent; const Button = React.forwardRef<unknown, ButtonProps>(InternalButton) as CompoundedComponent;
if (process.env.NODE_ENV !== 'production') {
Button.displayName = 'Button'; Button.displayName = 'Button';
}
Button.Group = Group; Button.Group = Group;
Button.__ANT_BUTTON = true; Button.__ANT_BUTTON = true;

View File

@ -50,4 +50,9 @@ When data is in the form of dates, such as schedules, timetables, prices calenda
## FAQ ## FAQ
- [How to use Calendar with customize date library](/docs/react/use-custom-date-library#Calendar) <<<<<<< HEAD
- # [How to use Calendar with customize date library](/docs/react/use-custom-date-library#Calendar)
- [How to use Calendar with customize date library like dayjs](/docs/react/replace-moment#Calendar)
- [How to set locale for date-related components](/components/date-picker/#Localization)
> > > > > > > origin/master

View File

@ -51,4 +51,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/dPQmLq08DI/Calendar.svg
## FAQ ## FAQ
- [如何在 Calendar 中使用自定义日期库](/docs/react/use-custom-date-library#Calendar) <<<<<<< HEAD
- # [如何在 Calendar 中使用自定义日期库](/docs/react/use-custom-date-library#Calendar)
- [如何在 Calendar 中使用自定义日期库(如 dayjs ](/docs/react/replace-moment#Calendar)
- [如何给日期类组件配置国际化](/components/date-picker/#%E5%9B%BD%E9%99%85%E5%8C%96%E9%85%8D%E7%BD%AE)
> > > > > > > origin/master

View File

@ -1,33 +1,34 @@
import * as React from 'react'; import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import RcCascader from 'rc-cascader';
import type { import type {
SingleCascaderProps as RcSingleCascaderProps,
MultipleCascaderProps as RcMultipleCascaderProps,
ShowSearchType,
FieldNames,
BaseOptionType, BaseOptionType,
DefaultOptionType, DefaultOptionType,
FieldNames,
MultipleCascaderProps as RcMultipleCascaderProps,
ShowSearchType,
SingleCascaderProps as RcSingleCascaderProps,
} from 'rc-cascader'; } from 'rc-cascader';
import RcCascader from 'rc-cascader';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import RightOutlined from '@ant-design/icons/RightOutlined'; import * as React from 'react';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import { useContext } from 'react'; import { useContext } from 'react';
import warning from '../_util/warning';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty'; import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext'; import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext'; import { FormItemInputContext } from '../form/context';
import getIcons from '../select/utils/iconUtil'; import getIcons from '../select/utils/iconUtil';
import type { SelectCommonPlacement } from '../_util/motion'; import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionName, getTransitionDirection } from '../_util/motion'; import { getTransitionDirection, getTransitionName } from '../_util/motion';
import { FormItemInputContext } from '../form/context';
import useStyle from './style';
import useSelectStyle from '../select/style';
import type { InputStatus } from '../_util/statusUtils'; import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import warning from '../_util/warning';
import useSelectStyle from '../select/style';
import useStyle from './style';
// Align the design since we use `rc-select` in root. This help: // Align the design since we use `rc-select` in root. This help:
// - List search content will show all content // - List search content will show all content
@ -317,8 +318,9 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
SHOW_PARENT: typeof SHOW_PARENT; SHOW_PARENT: typeof SHOW_PARENT;
SHOW_CHILD: typeof SHOW_CHILD; SHOW_CHILD: typeof SHOW_CHILD;
}; };
if (process.env.NODE_ENV !== 'production') {
Cascader.displayName = 'Cascader'; Cascader.displayName = 'Cascader';
}
Cascader.SHOW_PARENT = SHOW_PARENT; Cascader.SHOW_PARENT = SHOW_PARENT;
Cascader.SHOW_CHILD = SHOW_CHILD; Cascader.SHOW_CHILD = SHOW_CHILD;

View File

@ -1,11 +1,12 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import RcCheckbox from 'rc-checkbox'; import RcCheckbox from 'rc-checkbox';
import * as React from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
import { FormItemInputContext } from '../form/context';
import { GroupContext } from './Group';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import { FormItemInputContext } from '../form/context';
import warning from '../_util/warning'; import warning from '../_util/warning';
import { GroupContext } from './Group';
import useStyle from './style'; import useStyle from './style';
export interface AbstractCheckboxProps<T> { export interface AbstractCheckboxProps<T> {
@ -143,7 +144,8 @@ const InternalCheckbox: React.ForwardRefRenderFunction<HTMLInputElement, Checkbo
}; };
const Checkbox = React.forwardRef<unknown, CheckboxProps>(InternalCheckbox); const Checkbox = React.forwardRef<unknown, CheckboxProps>(InternalCheckbox);
if (process.env.NODE_ENV !== 'production') {
Checkbox.displayName = 'Checkbox'; Checkbox.displayName = 'Checkbox';
}
export default Checkbox; export default Checkbox;

View File

@ -117,8 +117,9 @@ export function withConfigConsumer<ExportProps extends BasicExportProps>(config:
const cons: ConstructorProps = Component.constructor as ConstructorProps; const cons: ConstructorProps = Component.constructor as ConstructorProps;
const name = (cons && cons.displayName) || Component.name || 'Component'; const name = (cons && cons.displayName) || Component.name || 'Component';
SFC.displayName = `withConfigConsumer(${name})`; if (process.env.NODE_ENV !== 'production') {
SFC.displayName = `withConfigConsumer(${name})`;
}
return SFC; return SFC;
}; };
} }

View File

@ -337,7 +337,8 @@ const Drawer = React.forwardRef<DrawerRef, DrawerProps>(
); );
}, },
); );
if (process.env.NODE_ENV !== 'production') {
Drawer.displayName = 'Drawer'; Drawer.displayName = 'Drawer';
}
export default Drawer; export default Drawer;

View File

@ -8194,6 +8194,232 @@ exports[`renders ./components/dropdown/demo/placement.md extend context correctl
</div> </div>
`; `;
exports[`renders ./components/dropdown/demo/selectable.md extend context correctly 1`] = `
Array [
<a
class="ant-typography ant-dropdown-trigger"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
Selectable
</div>
<div
class="ant-space-item"
>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</div>
</div>
</a>,
<div>
<div
class="ant-dropdown"
style="opacity:0"
>
<ul
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light"
data-menu-list="true"
role="menu"
tabindex="0"
>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 1
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 2
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-selected ant-dropdown-menu-item-only-child"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Item 3
</span>
</li>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
</ul>
<div
aria-hidden="true"
style="display:none"
>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
<div>
<div
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
/>
</div>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/dropdown/demo/sub-menu.md extend context correctly 1`] = ` exports[`renders ./components/dropdown/demo/sub-menu.md extend context correctly 1`] = `
Array [ Array [
<a <a

View File

@ -858,6 +858,46 @@ exports[`renders ./components/dropdown/demo/placement.md correctly 1`] = `
</div> </div>
`; `;
exports[`renders ./components/dropdown/demo/selectable.md correctly 1`] = `
<a
class="ant-typography ant-dropdown-trigger"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
Selectable
</div>
<div
class="ant-space-item"
>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</div>
</div>
</a>
`;
exports[`renders ./components/dropdown/demo/sub-menu.md correctly 1`] = ` exports[`renders ./components/dropdown/demo/sub-menu.md correctly 1`] = `
<a <a
class="ant-dropdown-trigger" class="ant-dropdown-trigger"

View File

@ -1,10 +1,10 @@
import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import React from 'react';
import Dropdown from '..'; import Dropdown from '..';
import Menu from '../../menu';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { sleep } from '../../../tests/utils'; import { act, fireEvent, render, sleep } from '../../../tests/utils';
import Menu from '../../menu';
describe('Dropdown', () => { describe('Dropdown', () => {
mountTest(() => ( mountTest(() => (
@ -99,4 +99,54 @@ describe('Dropdown', () => {
}), }),
); );
}); });
it('menu item with group', () => {
jest.useFakeTimers();
const { container } = render(
<Dropdown
trigger="click"
overlay={
<Menu
items={[
{
label: 'grp',
type: 'group',
children: [
{
label: '1',
key: 1,
},
],
},
]}
/>
}
>
<a />
</Dropdown>,
);
// Open
fireEvent.click(container.querySelector('a'));
act(() => {
jest.runAllTimers();
});
// Close
fireEvent.click(container.querySelector('.ant-dropdown-menu-item'));
// Force Motion move on
for (let i = 0; i < 10; i += 1) {
act(() => {
jest.runAllTimers();
});
}
// Motion End
fireEvent.animationEnd(container.querySelector('.ant-slide-up-leave-active'));
expect(container.querySelector('.ant-dropdown-hidden')).toBeTruthy();
jest.useRealTimers();
});
}); });

View File

@ -0,0 +1,54 @@
---
order: 10
title:
zh-CN: 菜单可选选择
en-US: Selectable Menu
---
## zh-CN
为 Menu 添加 `selectable` 属性可以开启选择能力。
## en-US
Config Menu `selectable` prop to enable selectable ability.
```tsx
import { DownOutlined } from '@ant-design/icons';
import { Dropdown, Menu, Space, Typography } from 'antd';
import React from 'react';
const menu = (
<Menu
selectable
defaultSelectedKeys={['3']}
items={[
{
key: '1',
label: 'Item 1',
},
{
key: '2',
label: 'Item 2',
},
{
key: '3',
label: 'Item 3',
},
]}
/>
);
const App: React.FC = () => (
<Dropdown overlay={menu}>
<Typography.Link>
<Space>
Selectable
<DownOutlined />
</Space>
</Typography.Link>
</Dropdown>
);
export default App;
```

View File

@ -1,10 +1,11 @@
import RightOutlined from '@ant-design/icons/RightOutlined'; import RightOutlined from '@ant-design/icons/RightOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import RcDropdown from 'rc-dropdown'; import RcDropdown from 'rc-dropdown';
import useEvent from 'rc-util/lib/hooks/useEvent';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import * as React from 'react'; import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import type { OverrideContextProps } from '../menu/OverrideContext'; import { OverrideProvider } from '../menu/OverrideContext';
import OverrideContext from '../menu/OverrideContext';
import getPlacements from '../_util/placements'; import getPlacements from '../_util/placements';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
@ -118,6 +119,8 @@ const Dropdown: DropdownInterface = props => {
disabled, disabled,
getPopupContainer, getPopupContainer,
overlayClassName, overlayClassName,
visible,
onVisibleChange,
} = props; } = props;
const prefixCls = getPrefixCls('dropdown', customizePrefixCls); const prefixCls = getPrefixCls('dropdown', customizePrefixCls);
@ -136,42 +139,35 @@ const Dropdown: DropdownInterface = props => {
disabled, disabled,
}); });
const overlayClassNameCustomized = classNames(overlayClassName, hashId, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const triggerActions = disabled ? [] : trigger; const triggerActions = disabled ? [] : trigger;
let alignPoint; let alignPoint;
if (triggerActions && triggerActions.indexOf('contextMenu') !== -1) { if (triggerActions && triggerActions.indexOf('contextMenu') !== -1) {
alignPoint = true; alignPoint = true;
} }
// =========================== Visible ============================
const [mergedVisible, setVisible] = useMergedState(false, {
value: visible,
});
const onInnerVisibleChange = useEvent((nextVisible: boolean) => {
onVisibleChange?.(nextVisible);
setVisible(nextVisible);
});
// =========================== Overlay ============================
const overlayClassNameCustomized = classNames(overlayClassName, hashId, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
const builtinPlacements = getPlacements({ const builtinPlacements = getPlacements({
arrowPointAtCenter: typeof arrow === 'object' && arrow.pointAtCenter, arrowPointAtCenter: typeof arrow === 'object' && arrow.pointAtCenter,
autoAdjustOverflow: true, autoAdjustOverflow: true,
}); });
const overlayContext = React.useMemo<OverrideContextProps>( const onMenuClick = React.useCallback(() => {
() => ({ setVisible(false);
prefixCls: `${prefixCls}-menu`, }, []);
expandIcon: (
<span className={`${prefixCls}-menu-submenu-arrow`}>
<RightOutlined className={`${prefixCls}-menu-submenu-arrow-icon`} />
</span>
),
mode: 'vertical',
selectable: false,
validator: ({ mode }) => {
// Warning if use other mode
warning(
!mode || mode === 'vertical',
'Dropdown',
`mode="${mode}" is not supported for Dropdown's Menu.`,
);
},
}),
[prefixCls],
);
const renderOverlay = () => { const renderOverlay = () => {
// rc-dropdown already can process the function of overlay, but we have check logic here. // rc-dropdown already can process the function of overlay, but we have check logic here.
@ -189,14 +185,36 @@ const Dropdown: DropdownInterface = props => {
); );
return ( return (
<OverrideContext.Provider value={overlayContext}>{overlayNode}</OverrideContext.Provider> <OverrideProvider
prefixCls={`${prefixCls}-menu`}
expandIcon={
<span className={`${prefixCls}-menu-submenu-arrow`}>
<RightOutlined className={`${prefixCls}-menu-submenu-arrow-icon`} />
</span>
}
mode="vertical"
selectable={false}
onClick={onMenuClick}
validator={({ mode }) => {
// Warning if use other mode
warning(
!mode || mode === 'vertical',
'Dropdown',
`mode="${mode}" is not supported for Dropdown's Menu.`,
);
}}
>
{overlayNode}
</OverrideProvider>
); );
}; };
// ============================ Render ============================
return wrapSSR( return wrapSSR(
<RcDropdown <RcDropdown
alignPoint={alignPoint} alignPoint={alignPoint}
{...props} {...props}
visible={mergedVisible}
builtinPlacements={builtinPlacements} builtinPlacements={builtinPlacements}
arrow={!!arrow} arrow={!!arrow}
overlayClassName={overlayClassNameCustomized} overlayClassName={overlayClassNameCustomized}
@ -206,6 +224,7 @@ const Dropdown: DropdownInterface = props => {
trigger={triggerActions} trigger={triggerActions}
overlay={renderOverlay} overlay={renderOverlay}
placement={getPlacement()} placement={getPlacement()}
onVisibleChange={onInnerVisibleChange}
> >
{dropdownTrigger} {dropdownTrigger}
</RcDropdown>, </RcDropdown>,

View File

@ -1,10 +1,10 @@
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface'; import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
import InternalForm, { useForm, FormInstance, FormProps, useWatch } from './Form';
import Item, { FormItemProps } from './FormItem';
import ErrorList, { ErrorListProps } from './ErrorList';
import List, { FormListProps } from './FormList';
import { FormProvider } from './context';
import warning from '../_util/warning'; import warning from '../_util/warning';
import { FormProvider } from './context';
import ErrorList, { ErrorListProps } from './ErrorList';
import InternalForm, { FormInstance, FormProps, useForm, useWatch } from './Form';
import Item, { FormItemProps } from './FormItem';
import List, { FormListFieldData, FormListOperation, FormListProps } from './FormList';
import useFormInstance from './hooks/useFormInstance'; import useFormInstance from './hooks/useFormInstance';
type InternalFormType = typeof InternalForm; type InternalFormType = typeof InternalForm;
@ -48,6 +48,8 @@ export {
RuleObject, RuleObject,
RuleRender, RuleRender,
FormListProps, FormListProps,
FormListFieldData,
FormListOperation,
}; };
export default Form; export default Form;

View File

@ -137,6 +137,8 @@ const Col = React.forwardRef<HTMLDivElement, ColProps>((props, ref) => {
); );
}); });
Col.displayName = 'Col'; if (process.env.NODE_ENV !== 'production') {
Col.displayName = 'Col';
}
export default Col; export default Col;

View File

@ -134,6 +134,8 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
); );
}); });
Row.displayName = 'Row'; if (process.env.NODE_ENV !== 'production') {
Row.displayName = 'Row';
}
export default Row; export default Row;

View File

@ -16,213 +16,154 @@ if (
} }
/* @remove-on-es-build-end */ /* @remove-on-es-build-end */
export type { AffixProps } from './affix';
export { default as Affix } from './affix'; export { default as Affix } from './affix';
export type { AffixProps } from './affix';
export type { AnchorProps, AnchorLinkProps } from './anchor';
export { default as Anchor } from './anchor';
export type { AutoCompleteProps } from './auto-complete';
export { default as AutoComplete } from './auto-complete';
export type { AlertProps } from './alert';
export { default as Alert } from './alert'; export { default as Alert } from './alert';
export type { AlertProps } from './alert';
export type { AvatarProps } from './avatar'; export { default as Anchor } from './anchor';
export type { AnchorLinkProps, AnchorProps } from './anchor';
export { default as AutoComplete } from './auto-complete';
export type { AutoCompleteProps } from './auto-complete';
export { default as Avatar } from './avatar'; export { default as Avatar } from './avatar';
export type { AvatarProps } from './avatar';
export type { BackTopProps } from './back-top';
export { default as BackTop } from './back-top'; export { default as BackTop } from './back-top';
export type { BackTopProps } from './back-top';
export type { BadgeProps } from './badge';
export { default as Badge } from './badge'; export { default as Badge } from './badge';
export type { BadgeProps } from './badge';
export type { BreadcrumbProps, BreadcrumbItemProps } from './breadcrumb';
export { default as Breadcrumb } from './breadcrumb'; export { default as Breadcrumb } from './breadcrumb';
export type { BreadcrumbItemProps, BreadcrumbProps } from './breadcrumb';
export type { ButtonProps } from './button';
export { default as Button } from './button'; export { default as Button } from './button';
export type { ButtonProps } from './button';
export type { CalendarProps } from './calendar';
export { default as Calendar } from './calendar'; export { default as Calendar } from './calendar';
export type { CalendarProps } from './calendar';
export type { CardProps } from './card';
export { default as Card } from './card'; export { default as Card } from './card';
export type { CardProps } from './card';
export type { CollapseProps, CollapsePanelProps } from './collapse';
export { default as Collapse } from './collapse';
export type { CarouselProps } from './carousel';
export { default as Carousel } from './carousel'; export { default as Carousel } from './carousel';
export type { CarouselProps } from './carousel';
export type { CascaderProps } from './cascader';
export { default as Cascader } from './cascader'; export { default as Cascader } from './cascader';
export type { CascaderProps } from './cascader';
export type { CheckboxProps, CheckboxOptionType } from './checkbox';
export { default as Checkbox } from './checkbox'; export { default as Checkbox } from './checkbox';
export type { CheckboxOptionType, CheckboxProps } from './checkbox';
export type { ColProps } from './col';
export { default as Col } from './col'; export { default as Col } from './col';
export type { ColProps } from './col';
export type { CommentProps } from './comment'; export { default as Collapse } from './collapse';
export type { CollapsePanelProps, CollapseProps } from './collapse';
export { default as Comment } from './comment'; export { default as Comment } from './comment';
export type { CommentProps } from './comment';
export { default as ConfigProvider } from './config-provider'; export { default as ConfigProvider } from './config-provider';
export type { DatePickerProps } from './date-picker';
export { default as DatePicker } from './date-picker'; export { default as DatePicker } from './date-picker';
export type { DatePickerProps } from './date-picker';
export type { DescriptionsProps } from './descriptions';
export { default as Descriptions } from './descriptions'; export { default as Descriptions } from './descriptions';
export type { DescriptionsProps } from './descriptions';
export type { DividerProps } from './divider';
export { default as Divider } from './divider'; export { default as Divider } from './divider';
export type { DividerProps } from './divider';
export { default as Drawer } from './drawer';
export type { DrawerProps } from './drawer';
export { default as Dropdown } from './dropdown';
export type { export type {
DropdownProps, DropdownProps,
// typo, but we need to support it for backwards compatibility // typo, but we need to support it for backwards compatibility
// https://github.com/ant-design/ant-design/pull/35161 // https://github.com/ant-design/ant-design/pull/35161
DropdownProps as DropDownProps, DropdownProps as DropDownProps,
} from './dropdown'; } from './dropdown';
export { default as Dropdown } from './dropdown';
export type { DrawerProps } from './drawer';
export { default as Drawer } from './drawer';
export type { EmptyProps } from './empty';
export { default as Empty } from './empty'; export { default as Empty } from './empty';
export type { EmptyProps } from './empty';
export type { FormInstance, FormProps, FormItemProps } from './form';
export { default as Form } from './form'; export { default as Form } from './form';
export { default as Grid } from './grid';
export type { InputProps, InputRef } from './input';
export { default as Input } from './input';
export type { ImageProps } from './image';
export { default as Image } from './image';
export type { InputNumberProps } from './input-number';
export { default as InputNumber } from './input-number';
export type { LayoutProps, SiderProps } from './layout';
export { default as Layout } from './layout';
export type { ListProps } from './list';
export { default as List } from './list';
export type { ArgsProps as MessageArgsProps } from './message';
export { default as message } from './message';
export type { MenuProps, MenuTheme, SubMenuProps, MenuItemProps } from './menu';
export { default as Menu } from './menu';
export type { MentionProps } from './mentions';
export { default as Mentions } from './mentions';
export type { ModalProps, ModalFuncProps } from './modal';
export { default as Modal } from './modal';
export type { StatisticProps } from './statistic';
export { default as Statistic } from './statistic';
export { default as notification } from './notification';
export type { PageHeaderProps } from './page-header';
export { default as PageHeader } from './page-header';
export type { PaginationProps } from './pagination';
export { default as Pagination } from './pagination';
export type { PopconfirmProps } from './popconfirm';
export { default as Popconfirm } from './popconfirm';
export type { PopoverProps } from './popover';
export { default as Popover } from './popover';
export type { ProgressProps } from './progress';
export { default as Progress } from './progress';
export type { RadioProps, RadioChangeEvent, RadioGroupProps } from './radio';
export { default as Radio } from './radio';
export type { RateProps } from './rate';
export { default as Rate } from './rate';
export type { ResultProps } from './result';
export { default as Result } from './result';
export type { RowProps } from './row';
export { default as Row } from './row';
export type { SelectProps } from './select';
export { default as Select } from './select';
export type { SegmentedProps } from './segmented';
export { default as Segmented } from './segmented';
export type { SkeletonProps } from './skeleton';
export { default as Skeleton } from './skeleton';
export type { SliderSingleProps } from './slider';
export { default as Slider } from './slider';
export type { SpaceProps } from './space';
export { default as Space } from './space';
export type { SpinProps } from './spin';
export { default as Spin } from './spin';
export type { StepProps, StepsProps } from './steps';
export { default as Steps } from './steps';
export type { SwitchProps } from './switch';
export { default as Switch } from './switch';
export type { export type {
TableProps, FormInstance,
TablePaginationConfig, FormItemProps,
FormListFieldData,
FormListOperation,
FormProps,
Rule as FormRule,
} from './form';
export { default as Grid } from './grid';
export { default as Image } from './image';
export type { ImageProps } from './image';
export { default as Input } from './input';
export type { InputProps, InputRef } from './input';
export { default as InputNumber } from './input-number';
export type { InputNumberProps } from './input-number';
export { default as Layout } from './layout';
export type { LayoutProps, SiderProps } from './layout';
export { default as List } from './list';
export type { ListProps } from './list';
export { default as Mentions } from './mentions';
export type { MentionProps } from './mentions';
export { default as Menu } from './menu';
export type { MenuItemProps, MenuProps, MenuTheme, SubMenuProps } from './menu';
export { default as message } from './message';
export type { ArgsProps as MessageArgsProps } from './message';
export { default as Modal } from './modal';
export type { ModalFuncProps, ModalProps } from './modal';
export { default as notification } from './notification';
export { default as PageHeader } from './page-header';
export type { PageHeaderProps } from './page-header';
export { default as Pagination } from './pagination';
export type { PaginationProps } from './pagination';
export { default as Popconfirm } from './popconfirm';
export type { PopconfirmProps } from './popconfirm';
export { default as Popover } from './popover';
export type { PopoverProps } from './popover';
export { default as Progress } from './progress';
export type { ProgressProps } from './progress';
export { default as Radio } from './radio';
export type { RadioChangeEvent, RadioGroupProps, RadioProps } from './radio';
export { default as Rate } from './rate';
export type { RateProps } from './rate';
export { default as Result } from './result';
export type { ResultProps } from './result';
export { default as Row } from './row';
export type { RowProps } from './row';
export { default as Segmented } from './segmented';
export type { SegmentedProps } from './segmented';
export { default as Select } from './select';
export type { RefSelectProps, SelectProps } from './select';
export { default as Skeleton } from './skeleton';
export type { SkeletonProps } from './skeleton';
export { default as Slider } from './slider';
export type { SliderSingleProps } from './slider';
export { default as Space } from './space';
export type { SpaceProps } from './space';
export { default as Spin } from './spin';
export type { SpinProps } from './spin';
export { default as Statistic } from './statistic';
export type { StatisticProps } from './statistic';
export { default as Steps } from './steps';
export type { StepProps, StepsProps } from './steps';
export { default as Switch } from './switch';
export type { SwitchProps } from './switch';
export { default as Table } from './table';
export type {
ColumnGroupType as TableColumnGroupType, ColumnGroupType as TableColumnGroupType,
ColumnType as TableColumnType,
ColumnProps as TableColumnProps, ColumnProps as TableColumnProps,
ColumnsType as TableColumnsType, ColumnsType as TableColumnsType,
ColumnType as TableColumnType,
TablePaginationConfig,
TableProps,
} from './table'; } from './table';
export { default as Table } from './table'; export { default as Tabs } from './tabs';
export type { TabPaneProps, TabsProps } from './tabs';
export type { TransferProps } from './transfer'; export { default as Tag } from './tag';
export type { TagProps, TagType } from './tag';
export { default as TimePicker } from './time-picker';
export type { TimePickerProps, TimeRangePickerProps } from './time-picker';
export { default as Timeline } from './timeline';
export type { TimelineItemProps, TimelineProps } from './timeline';
export { default as Tooltip } from './tooltip';
export type { TooltipProps } from './tooltip';
export { default as Transfer } from './transfer'; export { default as Transfer } from './transfer';
export type { TransferProps } from './transfer';
export { default as Tree } from './tree';
export type { export type {
TreeProps,
AntTreeNodeProps as TreeNodeProps, AntTreeNodeProps as TreeNodeProps,
DataNode as TreeDataNode, DataNode as TreeDataNode,
TreeProps,
} from './tree'; } from './tree';
export { default as Tree } from './tree';
export type { TreeSelectProps } from './tree-select';
export { default as TreeSelect } from './tree-select'; export { default as TreeSelect } from './tree-select';
export type { TreeSelectProps } from './tree-select';
export type { TabsProps, TabPaneProps } from './tabs';
export { default as Tabs } from './tabs';
export type { TagProps, TagType } from './tag';
export { default as Tag } from './tag';
export type { TimePickerProps, TimeRangePickerProps } from './time-picker';
export { default as TimePicker } from './time-picker';
export type { TimelineProps, TimelineItemProps } from './timeline';
export { default as Timeline } from './timeline';
export type { TooltipProps } from './tooltip';
export { default as Tooltip } from './tooltip';
export type { TypographyProps } from './typography';
export { default as Typography } from './typography'; export { default as Typography } from './typography';
export type { TypographyProps } from './typography';
export type { UploadProps } from './upload';
export { default as Upload } from './upload'; export { default as Upload } from './upload';
export type { UploadFile, UploadProps } from './upload';
export { default as version } from './version'; export { default as version } from './version';

View File

@ -11,9 +11,9 @@ import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext';
import { FormItemInputContext, NoFormStyle } from '../form/context'; import { FormItemInputContext, NoFormStyle } from '../form/context';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
import useStyle from './style';
import type { InputStatus } from '../_util/statusUtils'; import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import useStyle from './style';
type ValueType = string | number; type ValueType = string | number;
@ -95,7 +95,6 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
[`${prefixCls}-lg`]: mergeSize === 'large', [`${prefixCls}-lg`]: mergeSize === 'large',
[`${prefixCls}-sm`]: mergeSize === 'small', [`${prefixCls}-sm`]: mergeSize === 'small',
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-readonly`]: readOnly,
[`${prefixCls}-borderless`]: !bordered, [`${prefixCls}-borderless`]: !bordered,
[`${prefixCls}-in-form-item`]: isFormItemInput, [`${prefixCls}-in-form-item`]: isFormItemInput,
}, },

View File

@ -1,14 +1,14 @@
import * as React from 'react'; import EyeInvisibleOutlined from '@ant-design/icons/EyeInvisibleOutlined';
import EyeOutlined from '@ant-design/icons/EyeOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import EyeOutlined from '@ant-design/icons/EyeOutlined'; import * as React from 'react';
import EyeInvisibleOutlined from '@ant-design/icons/EyeInvisibleOutlined';
import { useState } from 'react'; import { useState } from 'react';
import type { InputRef, InputProps } from './Input';
import Input from './Input';
import type { ConfigConsumerProps } from '../config-provider'; import type { ConfigConsumerProps } from '../config-provider';
import { ConfigConsumer } from '../config-provider'; import { ConfigConsumer } from '../config-provider';
import type { InputProps, InputRef } from './Input';
import Input from './Input';
export interface PasswordProps extends InputProps { export interface PasswordProps extends InputProps {
readonly inputPrefixCls?: string; readonly inputPrefixCls?: string;
@ -98,6 +98,8 @@ Password.defaultProps = {
iconRender: (visible: boolean) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />), iconRender: (visible: boolean) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />),
}; };
Password.displayName = 'Password'; if (process.env.NODE_ENV !== 'production') {
Password.displayName = 'Password';
}
export default Password; export default Password;

View File

@ -1,13 +1,13 @@
import * as React from 'react'; import SearchOutlined from '@ant-design/icons/SearchOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
import SearchOutlined from '@ant-design/icons/SearchOutlined'; import * as React from 'react';
import Button from '../button';
import { ConfigContext } from '../config-provider';
import SizeContext from '../config-provider/SizeContext';
import { cloneElement } from '../_util/reactNode';
import type { InputProps, InputRef } from './Input'; import type { InputProps, InputRef } from './Input';
import Input from './Input'; import Input from './Input';
import Button from '../button';
import SizeContext from '../config-provider/SizeContext';
import { ConfigContext } from '../config-provider';
import { cloneElement } from '../_util/reactNode';
export interface SearchProps extends InputProps { export interface SearchProps extends InputProps {
inputPrefixCls?: string; inputPrefixCls?: string;
@ -165,7 +165,8 @@ const Search = React.forwardRef<InputRef, SearchProps>((props, ref) => {
/> />
); );
}); });
if (process.env.NODE_ENV !== 'production') {
Search.displayName = 'Search'; Search.displayName = 'Search';
}
export default Search; export default Search;

View File

@ -14,11 +14,13 @@ title:
Focus with additional option. Focus with additional option.
```tsx ```tsx
import type { InputRef } from 'antd';
import { Button, Input, Space, Switch } from 'antd'; import { Button, Input, Space, Switch } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
const App: React.FC = () => { const App: React.FC = () => {
const inputRef = useRef<any>(null); const inputRef = useRef<InputRef>(null);
const [input, setInput] = useState(true); const [input, setInput] = useState(true);
const sharedProps = { const sharedProps = {

View File

@ -1,14 +1,14 @@
import * as React from 'react'; import BarsOutlined from '@ant-design/icons/BarsOutlined';
import { useContext, useRef, useState, useEffect } from 'react'; import LeftOutlined from '@ant-design/icons/LeftOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import BarsOutlined from '@ant-design/icons/BarsOutlined'; import * as React from 'react';
import RightOutlined from '@ant-design/icons/RightOutlined'; import { useContext, useEffect, useRef, useState } from 'react';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import { LayoutContext } from './layout';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import isNumeric from '../_util/isNumeric'; import isNumeric from '../_util/isNumeric';
import { LayoutContext } from './layout';
const dimensionMaxMap = { const dimensionMaxMap = {
xs: '479.98px', xs: '479.98px',
@ -225,6 +225,8 @@ const Sider = React.forwardRef<HTMLDivElement, SiderProps>(
}, },
); );
Sider.displayName = 'Sider'; if (process.env.NODE_ENV !== 'production') {
Sider.displayName = 'Sider';
}
export default Sider; export default Sider;

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import useStyle from './style'; import useStyle from './style';
@ -39,7 +39,9 @@ function generator({ suffixCls, tagName, displayName }: GeneratorProps) {
return <BasicComponent ref={ref} prefixCls={prefixCls} tagName={tagName} {...props} />; return <BasicComponent ref={ref} prefixCls={prefixCls} tagName={tagName} {...props} />;
}); });
Adapter.displayName = displayName; if (process.env.NODE_ENV !== 'production') {
Adapter.displayName = displayName;
}
return Adapter; return Adapter;
}; };
} }

View File

@ -1,16 +1,17 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import RcMentions from 'rc-mentions'; import RcMentions from 'rc-mentions';
import type { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions'; import type { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
// eslint-disable-next-line import/no-named-as-default // eslint-disable-next-line import/no-named-as-default
import Spin from '../spin'; import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import { FormItemInputContext } from '../form/context'; import { FormItemInputContext } from '../form/context';
import useStyle from './style'; import Spin from '../spin';
import type { InputStatus } from '../_util/statusUtils'; import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import useStyle from './style';
export const { Option } = RcMentions; export const { Option } = RcMentions;
@ -173,7 +174,9 @@ const InternalMentions: React.ForwardRefRenderFunction<unknown, MentionProps> =
}; };
const Mentions = React.forwardRef<unknown, MentionProps>(InternalMentions) as CompoundedComponent; const Mentions = React.forwardRef<unknown, MentionProps>(InternalMentions) as CompoundedComponent;
Mentions.displayName = 'Mentions'; if (process.env.NODE_ENV !== 'production') {
Mentions.displayName = 'Mentions';
}
Mentions.Option = Option; Mentions.Option = Option;
Mentions.getMentions = (value: string = '', config: MentionsConfig = {}): MentionsEntity[] => { Mentions.getMentions = (value: string = '', config: MentionsConfig = {}): MentionsEntity[] => {

View File

@ -1,16 +0,0 @@
import * as React from 'react';
import type { MenuProps } from '.';
// Used for Dropdown only
export interface OverrideContextProps {
prefixCls?: string;
expandIcon?: React.ReactNode;
mode?: MenuProps['mode'];
selectable?: boolean;
validator?: (menuProps: Pick<MenuProps, 'mode'>) => void;
}
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
const OverrideContext = React.createContext<OverrideContextProps | null>(null);
export default OverrideContext;

View File

@ -0,0 +1,42 @@
import * as React from 'react';
import type { MenuProps } from '.';
// Used for Dropdown only
export interface OverrideContextProps {
prefixCls?: string;
expandIcon?: React.ReactNode;
mode?: MenuProps['mode'];
selectable?: boolean;
validator?: (menuProps: Pick<MenuProps, 'mode'>) => void;
onClick?: () => void;
}
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
const OverrideContext = React.createContext<OverrideContextProps | null>(null);
/** @private Internal Usage. Only used for Dropdown component. Do not use this in your production. */
export const OverrideProvider = ({
children,
...restProps
}: OverrideContextProps & { children: React.ReactNode }) => {
const override = React.useContext(OverrideContext);
const context = React.useMemo(
() => ({
...override,
...restProps,
}),
[
override,
restProps.prefixCls,
// restProps.expandIcon, Not mark as deps since this is a ReactNode
restProps.mode,
restProps.selectable,
// restProps.validator, Not mark as deps since this is a function
],
);
return <OverrideContext.Provider value={context}>{children}</OverrideContext.Provider>;
};
export default OverrideContext;

View File

@ -1,28 +1,30 @@
import * as React from 'react'; import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined';
import classNames from 'classnames';
import type { MenuProps as RcMenuProps, MenuRef } from 'rc-menu'; import type { MenuProps as RcMenuProps, MenuRef } from 'rc-menu';
import RcMenu, { ItemGroup } from 'rc-menu'; import RcMenu, { ItemGroup } from 'rc-menu';
import classNames from 'classnames'; import useEvent from 'rc-util/lib/hooks/useEvent';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined'; import * as React from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import SubMenu, { SubMenuProps } from './SubMenu';
import Item, { MenuItemProps } from './MenuItem';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import warning from '../_util/warning';
import type { SiderContextProps } from '../layout/Sider'; import type { SiderContextProps } from '../layout/Sider';
import { SiderContext } from '../layout/Sider'; import { SiderContext } from '../layout/Sider';
import collapseMotion from '../_util/motion'; import collapseMotion from '../_util/motion';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
import MenuContext, { MenuTheme } from './MenuContext'; import warning from '../_util/warning';
import MenuDivider from './MenuDivider';
import type { ItemType } from './hooks/useItems'; import type { ItemType } from './hooks/useItems';
import useItems from './hooks/useItems'; import useItems from './hooks/useItems';
import MenuContext, { MenuTheme } from './MenuContext';
import MenuDivider from './MenuDivider';
import Item, { MenuItemProps } from './MenuItem';
import OverrideContext from './OverrideContext'; import OverrideContext from './OverrideContext';
import SubMenu, { SubMenuProps } from './SubMenu';
import useStyle from './style'; import useStyle from './style';
export { MenuDividerProps } from './MenuDivider';
export { MenuItemGroupProps } from 'rc-menu'; export { MenuItemGroupProps } from 'rc-menu';
export { MenuDividerProps } from './MenuDivider';
export { MenuTheme, SubMenuProps, MenuItemProps };
export type MenuMode = 'vertical' | 'vertical-left' | 'vertical-right' | 'horizontal' | 'inline'; export type MenuMode = 'vertical' | 'vertical-left' | 'vertical-right' | 'horizontal' | 'inline';
@ -46,8 +48,9 @@ type InternalMenuProps = MenuProps &
}; };
const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => { const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => {
const override = React.useContext(OverrideContext); const override = React.useContext(OverrideContext) || {};
const overrideObj = override || {}; const overrideObj = override || {};
const { getPrefixCls, getPopupContainer, direction } = React.useContext(ConfigContext); const { getPrefixCls, getPopupContainer, direction } = React.useContext(ConfigContext);
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
@ -65,6 +68,7 @@ const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => {
rootClassName, rootClassName,
mode, mode,
selectable, selectable,
onClick,
...restProps ...restProps
} = props; } = props;
@ -94,6 +98,13 @@ const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => {
overrideObj.validator?.({ mode }); overrideObj.validator?.({ mode });
// ========================== Click ==========================
// Tell dropdown that item clicked
const onItemClick = useEvent<Required<MenuProps>['onClick']>((...args) => {
onClick?.(...args);
override?.onClick?.();
});
// ========================== Mode =========================== // ========================== Mode ===========================
const mergedMode = overrideObj.mode || mode; const mergedMode = overrideObj.mode || mode;
@ -152,6 +163,7 @@ const InternalMenu = forwardRef<MenuRef, InternalMenuProps>((props, ref) => {
overflowedIndicatorPopupClassName={`${prefixCls}-${theme}`} overflowedIndicatorPopupClassName={`${prefixCls}-${theme}`}
mode={mergedMode} mode={mergedMode}
selectable={mergedSelectable} selectable={mergedSelectable}
onClick={onItemClick}
{...passedProps} {...passedProps}
inlineCollapsed={mergedInlineCollapsed} inlineCollapsed={mergedInlineCollapsed}
className={menuClassName} className={menuClassName}
@ -202,6 +214,4 @@ class Menu extends React.Component<MenuProps, {}> {
} }
} }
export { MenuTheme, SubMenuProps, MenuItemProps };
export default Menu; export default Menu;

View File

@ -1,10 +1,10 @@
import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { spyElementPrototype } from 'rc-util/lib/test/domHook'; import { spyElementPrototype } from 'rc-util/lib/test/domHook';
import React from 'react';
import Popconfirm from '..'; import Popconfirm from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import { sleep, render, fireEvent } from '../../../tests/utils';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import Button from '../../button'; import Button from '../../button';
describe('Popconfirm', () => { describe('Popconfirm', () => {
@ -263,7 +263,7 @@ describe('Popconfirm', () => {
fireEvent.click(container.querySelector('.ant-btn-primary')); fireEvent.click(container.querySelector('.ant-btn-primary'));
await sleep(500); await sleep(500);
expect(container.textContent).toEqual('Unmounted'); // expect(container.textContent).toEqual('Unmounted');
expect(error).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled();
}); });
}); });

View File

@ -53,7 +53,9 @@ const Popover = React.forwardRef<unknown, PopoverProps>(
}, },
); );
Popover.displayName = 'Popover'; if (process.env.NODE_ENV !== 'production') {
Popover.displayName = 'Popover';
}
Popover.defaultProps = { Popover.defaultProps = {
placement: 'top' as TooltipPlacement, placement: 'top' as TooltipPlacement,

View File

@ -1,14 +1,15 @@
import * as React from 'react';
import RcCheckbox from 'rc-checkbox';
import classNames from 'classnames'; import classNames from 'classnames';
import RcCheckbox from 'rc-checkbox';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
import { FormItemInputContext } from '../form/context';
import type { RadioProps, RadioChangeEvent } from './interface';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import RadioGroupContext, { RadioOptionTypeContext } from './context';
import DisabledContext from '../config-provider/DisabledContext'; import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext } from '../form/context';
import warning from '../_util/warning'; import warning from '../_util/warning';
import RadioGroupContext, { RadioOptionTypeContext } from './context';
import type { RadioChangeEvent, RadioProps } from './interface';
import useStyle from './style'; import useStyle from './style';
const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (props, ref) => { const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (props, ref) => {
@ -84,6 +85,8 @@ const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (
const Radio = React.forwardRef<unknown, RadioProps>(InternalRadio); const Radio = React.forwardRef<unknown, RadioProps>(InternalRadio);
Radio.displayName = 'Radio'; if (process.env.NODE_ENV !== 'production') {
Radio.displayName = 'Radio';
}
export default Radio; export default Radio;

View File

@ -39,7 +39,9 @@ const Rate = React.forwardRef<unknown, RateProps>(({ prefixCls, tooltips, ...pro
); );
}); });
Rate.displayName = 'Rate'; if (process.env.NODE_ENV !== 'production') {
Rate.displayName = 'Rate';
}
Rate.defaultProps = { Rate.defaultProps = {
character: <StarFilled />, character: <StarFilled />,

View File

@ -1,11 +1,11 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import RcSegmented from 'rc-segmented';
import type { import type {
SegmentedLabeledOption as RcSegmentedLabeledOption,
SegmentedProps as RCSegmentedProps, SegmentedProps as RCSegmentedProps,
SegmentedRawOption, SegmentedRawOption,
SegmentedLabeledOption as RcSegmentedLabeledOption,
} from 'rc-segmented'; } from 'rc-segmented';
import RcSegmented from 'rc-segmented';
import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import type { SizeType } from '../config-provider/SizeContext'; import type { SizeType } from '../config-provider/SizeContext';
@ -103,7 +103,10 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>((props, ref)
); );
}); });
Segmented.displayName = 'Segmented'; if (process.env.NODE_ENV !== 'production') {
Segmented.displayName = 'Segmented';
}
Segmented.defaultProps = { Segmented.defaultProps = {
options: [], options: [],
}; };

View File

@ -263,7 +263,7 @@
// .skeleton-color() { // .skeleton-color() {
// position: relative; // position: relative;
// overflow: hidden; // overflow: hidden;
// background: #fff; // background: transparent;
// &::after { // &::after {
// position: absolute; // position: absolute;
@ -276,9 +276,9 @@
// @skeleton-color 25%, // @skeleton-color 25%,
// @skeleton-to-color 37%, // @skeleton-to-color 37%,
// @skeleton-color 63% // @skeleton-color 63%
// ); // );
// animation: ~'@{skeleton-prefix-cls}-loading' 1.4s ease infinite; // animation: ~'@{skeleton-prefix-cls}-loading' 1.4s ease infinite;
// content: ""; // content: '';
// } // }
// } // }

View File

@ -1,10 +1,11 @@
import * as React from 'react'; import classNames from 'classnames';
import type { SliderProps as RcSliderProps } from 'rc-slider'; import type { SliderProps as RcSliderProps } from 'rc-slider';
import RcSlider from 'rc-slider'; import RcSlider from 'rc-slider';
import classNames from 'classnames'; import * as React from 'react';
import { ConfigContext } from '../config-provider';
import type { TooltipPlacement } from '../tooltip'; import type { TooltipPlacement } from '../tooltip';
import SliderTooltip from './SliderTooltip'; import SliderTooltip from './SliderTooltip';
import { ConfigContext } from '../config-provider';
import useStyle from './style'; import useStyle from './style';
export type SliderMarks = RcSliderProps['marks']; export type SliderMarks = RcSliderProps['marks'];
@ -169,7 +170,9 @@ const Slider = React.forwardRef<unknown, SliderSingleProps | SliderRangeProps>(
}, },
); );
Slider.displayName = 'Slider'; if (process.env.NODE_ENV !== 'production') {
Slider.displayName = 'Slider';
}
Slider.defaultProps = { Slider.defaultProps = {
tipFormatter(value: number) { tipFormatter(value: number) {

View File

@ -66,6 +66,15 @@
// &.@{ant-prefix}-tooltip-open { // &.@{ant-prefix}-tooltip-open {
// border-color: @slider-handle-color-tooltip-open; // border-color: @slider-handle-color-tooltip-open;
// } // }
// &::after {
// position: absolute;
// top: -6px;
// right: -6px;
// bottom: -6px;
// left: -6px;
// content: "";
// }
// } // }
// &:hover { // &:hover {

View File

@ -1,13 +1,14 @@
import * as React from 'react';
import RcSwitch from 'rc-switch';
import classNames from 'classnames';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import RcSwitch from 'rc-switch';
import * as React from 'react';
import Wave from '../_util/wave';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext'; import DisabledContext from '../config-provider/DisabledContext';
import SizeContext from '../config-provider/SizeContext';
import warning from '../_util/warning'; import warning from '../_util/warning';
import Wave from '../_util/wave';
import useStyle from './style'; import useStyle from './style';
export type SwitchSize = 'small' | 'default'; export type SwitchSize = 'small' | 'default';
@ -99,6 +100,8 @@ const Switch = React.forwardRef<unknown, SwitchProps>(
) as CompoundedComponent; ) as CompoundedComponent;
Switch.__ANT_SWITCH = true; Switch.__ANT_SWITCH = true;
Switch.displayName = 'Switch'; if (process.env.NODE_ENV !== 'production') {
Switch.displayName = 'Switch';
}
export default Switch; export default Switch;

View File

@ -1,13 +1,14 @@
/* eslint-disable react/no-multi-comp */ /* eslint-disable react/no-multi-comp */
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { render, fireEvent, waitFor } from '../../../tests/utils';
import Table from '..'; import Table from '..';
import Input from '../../input'; import { fireEvent, render, waitFor } from '../../../tests/utils';
import Tooltip from '../../tooltip';
import Button from '../../button'; import Button from '../../button';
import Select from '../../select';
import ConfigProvider from '../../config-provider'; import ConfigProvider from '../../config-provider';
import Input from '../../input';
import Menu from '../../menu';
import Select from '../../select';
import Tooltip from '../../tooltip';
// https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73 // https://github.com/Semantic-Org/Semantic-UI-React/blob/72c45080e4f20b531fda2e3e430e384083d6766b/test/specs/modules/Dropdown/Dropdown-test.js#L73
const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } }; const nativeEvent = { nativeEvent: { stopImmediatePropagation: () => {} } };
@ -65,6 +66,14 @@ describe('Table.filter', () => {
return namesList; return namesList;
} }
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('not show filter icon when undefined', () => { it('not show filter icon when undefined', () => {
const noFilterColumn = { ...column, filters: undefined }; const noFilterColumn = { ...column, filters: undefined };
delete noFilterColumn.onFilter; delete noFilterColumn.onFilter;
@ -106,8 +115,6 @@ describe('Table.filter', () => {
}); });
it('renders empty menu correctly', () => { it('renders empty menu correctly', () => {
jest.useFakeTimers();
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -129,8 +136,6 @@ describe('Table.filter', () => {
expect(container.querySelector('.ant-empty')).toBeTruthy(); expect(container.querySelector('.ant-empty')).toBeTruthy();
expect(errorSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore(); errorSpy.mockRestore();
jest.useRealTimers();
}); });
it('renders radio filter correctly', async () => { it('renders radio filter correctly', async () => {
@ -613,7 +618,6 @@ describe('Table.filter', () => {
onChange, onChange,
}), }),
); );
jest.useFakeTimers();
expect(renderedNames(container)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']); expect(renderedNames(container)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
@ -665,8 +669,6 @@ describe('Table.filter', () => {
// What's this? Is that a coverage case? Or check a crash? // What's this? Is that a coverage case? Or check a crash?
const latestItems = getFilterMenu().querySelectorAll('li.ant-dropdown-menu-item'); const latestItems = getFilterMenu().querySelectorAll('li.ant-dropdown-menu-item');
fireEvent.click(latestItems[latestItems.length - 1]); fireEvent.click(latestItems[latestItems.length - 1]);
jest.useRealTimers();
}); });
describe('should support value types', () => { describe('should support value types', () => {
@ -676,7 +678,6 @@ describe('Table.filter', () => {
['Bamboo', false], ['Bamboo', false],
].forEach(([text, value]) => { ].forEach(([text, value]) => {
it(`${typeof value} type`, async () => { it(`${typeof value} type`, async () => {
jest.useFakeTimers();
const onChange = jest.fn(); const onChange = jest.fn();
const filters = [{ text, value }]; const filters = [{ text, value }];
const { container } = render( const { container } = render(
@ -698,8 +699,6 @@ describe('Table.filter', () => {
fireEvent.click(container.querySelector('.ant-dropdown-trigger')); fireEvent.click(container.querySelector('.ant-dropdown-trigger'));
jest.useFakeTimers();
fireEvent.click(container.querySelectorAll('.ant-dropdown-menu-item')[0]); fireEvent.click(container.querySelectorAll('.ant-dropdown-menu-item')[0]);
// This test can be remove if refactor // This test can be remove if refactor
@ -733,7 +732,6 @@ describe('Table.filter', () => {
.querySelector('.ant-table-filter-dropdown') .querySelector('.ant-table-filter-dropdown')
.querySelectorAll('.ant-checkbox-input')[0].checked, .querySelectorAll('.ant-checkbox-input')[0].checked,
).toEqual(false); ).toEqual(false);
jest.useRealTimers();
}); });
}); });
}); });
@ -1831,7 +1829,6 @@ describe('Table.filter', () => {
describe('filter tree mode', () => { describe('filter tree mode', () => {
it('supports filter tree', () => { it('supports filter tree', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -1852,7 +1849,6 @@ describe('Table.filter', () => {
}); });
it('supports search input in filter tree', () => { it('supports search input in filter tree', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -1875,7 +1871,6 @@ describe('Table.filter', () => {
}); });
it('supports search input in filter menu', () => { it('supports search input in filter menu', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -1897,7 +1892,6 @@ describe('Table.filter', () => {
}); });
it('should skip search when filters[0].text is ReactNode', () => { it('should skip search when filters[0].text is ReactNode', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -1936,7 +1930,6 @@ describe('Table.filter', () => {
}); });
it('should supports filterSearch has type of function', () => { it('should supports filterSearch has type of function', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -1974,7 +1967,6 @@ describe('Table.filter', () => {
}); });
it('supports check all items', () => { it('supports check all items', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2007,7 +1999,6 @@ describe('Table.filter', () => {
}); });
it('supports check item by selecting it', () => { it('supports check item by selecting it', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2043,7 +2034,6 @@ describe('Table.filter', () => {
}); });
it('select-all checkbox should change when all items are selected', () => { it('select-all checkbox should change when all items are selected', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2075,7 +2065,6 @@ describe('Table.filter', () => {
}); });
it('filterMultiple is false - check item', () => { it('filterMultiple is false - check item', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2123,7 +2112,6 @@ describe('Table.filter', () => {
}); });
it('filterMultiple is false - select item', () => { it('filterMultiple is false - select item', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2170,7 +2158,6 @@ describe('Table.filter', () => {
}); });
it('should select children when select parent', () => { it('should select children when select parent', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const { container } = render( const { container } = render(
createTable({ createTable({
@ -2299,7 +2286,6 @@ describe('Table.filter', () => {
}); });
it('filterDropdown should support filterResetToDefaultFilteredValue', () => { it('filterDropdown should support filterResetToDefaultFilteredValue', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined); jest.spyOn(console, 'error').mockImplementation(() => undefined);
const columnFilter = { const columnFilter = {
@ -2347,6 +2333,44 @@ describe('Table.filter', () => {
expect(container.querySelector('.ant-tree-checkbox-checked+span').textContent).toBe('Girl'); expect(container.querySelector('.ant-tree-checkbox-checked+span').textContent).toBe('Girl');
}); });
it('filterDropdown should not override customize Menu selectable', () => {
const onSelect = jest.fn();
const { container } = render(
createTable({
columns: [
{
...column,
filterDropdown: (
<div className="custom-filter-dropdown">
<Menu
onSelect={onSelect}
items={[
{
key: '1',
label: 'Item 1',
},
]}
/>
</div>
),
},
],
}),
);
// Open Filter
fireEvent.click(container.querySelector('span.ant-dropdown-trigger'));
act(() => {
jest.runAllTimers();
});
// Click Item
fireEvent.click(container.querySelector('.ant-table-filter-dropdown .ant-dropdown-menu-item'));
expect(onSelect).toHaveBeenCalled();
});
it('filteredKeys should all be controlled or not controlled', () => { it('filteredKeys should all be controlled or not controlled', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
errorSpy.mockReset(); errorSpy.mockReset();

View File

@ -40,7 +40,12 @@ const VirtualTable = (props: Parameters<typeof Table>[0]) => {
const [connectObject] = useState<any>(() => { const [connectObject] = useState<any>(() => {
const obj = {}; const obj = {};
Object.defineProperty(obj, 'scrollLeft', { Object.defineProperty(obj, 'scrollLeft', {
get: () => null, get: () => {
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft;
}
return null;
},
set: (scrollLeft: number) => { set: (scrollLeft: number) => {
if (gridRef.current) { if (gridRef.current) {
gridRef.current.scrollTo({ scrollLeft }); gridRef.current.scrollTo({ scrollLeft });

View File

@ -1,8 +1,8 @@
import FilterFilled from '@ant-design/icons/FilterFilled'; import FilterFilled from '@ant-design/icons/FilterFilled';
import classNames from 'classnames'; import classNames from 'classnames';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import * as React from 'react';
import type { FieldDataNode } from 'rc-tree'; import type { FieldDataNode } from 'rc-tree';
import * as React from 'react';
import type { FilterState } from '.'; import type { FilterState } from '.';
import { flattenKeys } from '.'; import { flattenKeys } from '.';
import Button from '../../../button'; import Button from '../../../button';
@ -13,6 +13,7 @@ import Dropdown from '../../../dropdown';
import Empty from '../../../empty'; import Empty from '../../../empty';
import type { MenuProps } from '../../../menu'; import type { MenuProps } from '../../../menu';
import Menu from '../../../menu'; import Menu from '../../../menu';
import { OverrideProvider } from '../../../menu/OverrideContext';
import Radio from '../../../radio'; import Radio from '../../../radio';
import type { EventDataNode } from '../../../tree'; import type { EventDataNode } from '../../../tree';
import Tree from '../../../tree'; import Tree from '../../../tree';
@ -183,21 +184,9 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
// ====================== Open Keys ====================== // ====================== Open Keys ======================
const [openKeys, setOpenKeys] = React.useState<string[]>([]); const [openKeys, setOpenKeys] = React.useState<string[]>([]);
const openRef = React.useRef<number>();
const onOpenChange = (keys: string[]) => { const onOpenChange = (keys: string[]) => {
openRef.current = window.setTimeout(() => { setOpenKeys(keys);
setOpenKeys(keys);
});
}; };
const onMenuClick = () => {
window.clearTimeout(openRef.current);
};
React.useEffect(
() => () => {
window.clearTimeout(openRef.current);
},
[],
);
// search in tree mode column filter // search in tree mode column filter
const [searchValue, setSearchValue] = React.useState(''); const [searchValue, setSearchValue] = React.useState('');
@ -303,6 +292,7 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
}); });
let dropdownContent: React.ReactNode; let dropdownContent: React.ReactNode;
if (typeof column.filterDropdown === 'function') { if (typeof column.filterDropdown === 'function') {
dropdownContent = column.filterDropdown({ dropdownContent = column.filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`, prefixCls: `${dropdownPrefixCls}-custom`,
@ -395,7 +385,6 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
multiple={filterMultiple} multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`} prefixCls={`${dropdownPrefixCls}-menu`}
className={dropdownMenuClass} className={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys} onSelect={onSelectKeys}
onDeselect={onSelectKeys} onDeselect={onSelectKeys}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
@ -441,6 +430,11 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
); );
} }
// We should not block customize Menu with additional props
if (column.filterDropdown) {
dropdownContent = <OverrideProvider selectable={undefined}>{dropdownContent}</OverrideProvider>;
}
const menu = ( const menu = (
<FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}> <FilterDropdownMenuWrapper className={`${prefixCls}-dropdown`}>
{dropdownContent} {dropdownContent}

View File

@ -1,14 +1,14 @@
import * as React from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames'; import classNames from 'classnames';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import CloseOutlined from '@ant-design/icons/CloseOutlined'; import * as React from 'react';
import CheckableTag from './CheckableTag';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import type { PresetColorType, PresetStatusColorType } from '../_util/colors'; import type { PresetColorType, PresetStatusColorType } from '../_util/colors';
import { PresetColorTypes, PresetStatusColorTypes } from '../_util/colors'; import { PresetColorTypes, PresetStatusColorTypes } from '../_util/colors';
import Wave from '../_util/wave';
import type { LiteralUnion } from '../_util/type'; import type { LiteralUnion } from '../_util/type';
import Wave from '../_util/wave';
import CheckableTag from './CheckableTag';
import useStyle from './style'; import useStyle from './style';
@ -137,7 +137,9 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
const Tag = React.forwardRef<unknown, TagProps>(InternalTag) as TagType; const Tag = React.forwardRef<unknown, TagProps>(InternalTag) as TagType;
Tag.displayName = 'Tag'; if (process.env.NODE_ENV !== 'production') {
Tag.displayName = 'Tag';
}
Tag.CheckableTag = CheckableTag; Tag.CheckableTag = CheckableTag;

View File

@ -2,8 +2,8 @@ import type { Dayjs } from 'dayjs';
import * as React from 'react'; import * as React from 'react';
import DatePicker from '../date-picker'; import DatePicker from '../date-picker';
import type { PickerTimeProps, RangePickerTimeProps } from '../date-picker/generatePicker'; import type { PickerTimeProps, RangePickerTimeProps } from '../date-picker/generatePicker';
import warning from '../_util/warning';
import type { InputStatus } from '../_util/statusUtils'; import type { InputStatus } from '../_util/statusUtils';
import warning from '../_util/warning';
const { TimePicker: InternalTimePicker, RangePicker: InternalRangePicker } = DatePicker; const { TimePicker: InternalTimePicker, RangePicker: InternalRangePicker } = DatePicker;
@ -61,7 +61,9 @@ const TimePicker = React.forwardRef<any, TimePickerProps>(
}, },
); );
TimePicker.displayName = 'TimePicker'; if (process.env.NODE_ENV !== 'production') {
TimePicker.displayName = 'TimePicker';
}
type MergedTimePicker = typeof TimePicker & { type MergedTimePicker = typeof TimePicker & {
RangePicker: typeof RangePicker; RangePicker: typeof RangePicker;

View File

@ -11,6 +11,7 @@ import { getTransitionName } from '../_util/motion';
import getPlacements, { AdjustOverflow, PlacementsConfig } from '../_util/placements'; import getPlacements, { AdjustOverflow, PlacementsConfig } from '../_util/placements';
import { cloneElement, isValidElement } from '../_util/reactNode'; import { cloneElement, isValidElement } from '../_util/reactNode';
import type { LiteralUnion } from '../_util/type'; import type { LiteralUnion } from '../_util/type';
import useStyle from './style'; import useStyle from './style';
export { AdjustOverflow, PlacementsConfig }; export { AdjustOverflow, PlacementsConfig };
@ -282,7 +283,9 @@ const Tooltip = React.forwardRef<unknown, TooltipProps>((props, ref) => {
); );
}); });
Tooltip.displayName = 'Tooltip'; if (process.env.NODE_ENV !== 'production') {
Tooltip.displayName = 'Tooltip';
}
Tooltip.defaultProps = { Tooltip.defaultProps = {
placement: 'top' as TooltipPlacement, placement: 'top' as TooltipPlacement,

View File

@ -1,24 +1,32 @@
import * as React from 'react';
import classNames from 'classnames';
import type RcTree from 'rc-tree';
import { conductExpandParent } from 'rc-tree/lib/util';
import type { EventDataNode, DataNode, Key } from 'rc-tree/lib/interface';
import { convertDataToEntities, convertTreeToData } from 'rc-tree/lib/utils/treeUtil';
import FileOutlined from '@ant-design/icons/FileOutlined'; import FileOutlined from '@ant-design/icons/FileOutlined';
import FolderOpenOutlined from '@ant-design/icons/FolderOpenOutlined'; import FolderOpenOutlined from '@ant-design/icons/FolderOpenOutlined';
import FolderOutlined from '@ant-design/icons/FolderOutlined'; import FolderOutlined from '@ant-design/icons/FolderOutlined';
import classNames from 'classnames';
import type RcTree from 'rc-tree';
import type { BasicDataNode } from 'rc-tree';
import type { DataNode, EventDataNode, Key } from 'rc-tree/lib/interface';
import { conductExpandParent } from 'rc-tree/lib/util';
import { convertDataToEntities, convertTreeToData } from 'rc-tree/lib/utils/treeUtil';
import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import type { TreeProps, AntdTreeNodeAttribute } from './Tree'; import type { AntdTreeNodeAttribute, TreeProps } from './Tree';
import Tree from './Tree'; import Tree from './Tree';
import { calcRangeKeys, convertDirectoryKeysToNodes } from './utils/dictUtil'; import { calcRangeKeys, convertDirectoryKeysToNodes } from './utils/dictUtil';
export type ExpandAction = false | 'click' | 'doubleClick'; export type ExpandAction = false | 'click' | 'doubleClick';
export interface DirectoryTreeProps extends TreeProps { export interface DirectoryTreeProps<T extends BasicDataNode = DataNode> extends TreeProps<T> {
expandAction?: ExpandAction; expandAction?: ExpandAction;
} }
type DirectoryTreeCompoundedComponent = (<T extends BasicDataNode | DataNode = DataNode>(
props: React.PropsWithChildren<DirectoryTreeProps<T>> & { ref?: React.Ref<RcTree> },
) => React.ReactElement) & {
defaultProps: Partial<React.PropsWithChildren<DirectoryTreeProps<any>>>;
displayName?: string;
};
export interface DirectoryTreeState { export interface DirectoryTreeState {
expandedKeys?: Key[]; expandedKeys?: Key[];
selectedKeys?: Key[]; selectedKeys?: Key[];
@ -191,8 +199,13 @@ const DirectoryTree: React.ForwardRefRenderFunction<RcTree, DirectoryTreeProps>
); );
}; };
const ForwardDirectoryTree = React.forwardRef(DirectoryTree); const ForwardDirectoryTree = React.forwardRef(
ForwardDirectoryTree.displayName = 'DirectoryTree'; DirectoryTree,
) as unknown as DirectoryTreeCompoundedComponent;
if (process.env.NODE_ENV !== 'production') {
ForwardDirectoryTree.displayName = 'DirectoryTree';
}
ForwardDirectoryTree.defaultProps = { ForwardDirectoryTree.defaultProps = {
showIcon: true, showIcon: true,

View File

@ -3,6 +3,8 @@ import * as React from 'react';
import { render } from '../../../tests/utils'; import { render } from '../../../tests/utils';
import Tree from '../index'; import Tree from '../index';
const { DirectoryTree } = Tree;
describe('Tree.TypeScript', () => { describe('Tree.TypeScript', () => {
it('without generic', () => { it('without generic', () => {
const { container } = render( const { container } = render(
@ -48,4 +50,28 @@ describe('Tree.TypeScript', () => {
expect(container).toBeTruthy(); expect(container).toBeTruthy();
}); });
it('directoryTree support generic', () => {
interface MyDataNode extends BasicDataNode {
bamboo: string;
list?: MyDataNode[];
}
const { container } = render(
<DirectoryTree<MyDataNode>
treeData={[
{
bamboo: 'good',
list: [
{
bamboo: 'well',
},
],
},
]}
/>,
);
expect(container).toBeTruthy();
});
}); });

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { composeRef } from 'rc-util/lib/ref'; import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import warning from '../_util/warning'; import warning from '../_util/warning';
import useStyle from './style'; import useStyle from './style';
@ -62,8 +62,9 @@ const Typography: React.ForwardRefRenderFunction<{}, InternalTypographyProps> =
}; };
const RefTypography = React.forwardRef(Typography); const RefTypography = React.forwardRef(Typography);
if (process.env.NODE_ENV !== 'production') {
RefTypography.displayName = 'Typography'; RefTypography.displayName = 'Typography';
}
// es default export should use const instead of let // es default export should use const instead of let
const ExportTypography = RefTypography as unknown as React.FC<TypographyProps>; const ExportTypography = RefTypography as unknown as React.FC<TypographyProps>;

View File

@ -1,4 +1,5 @@
import { LikeOutlined, SmileOutlined } from '@ant-design/icons'; import { LikeOutlined, SmileOutlined } from '@ant-design/icons';
import { act } from '@testing-library/react';
import * as copyObj from 'copy-to-clipboard'; import * as copyObj from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
import { fireEvent, render, waitFor } from '../../../tests/utils'; import { fireEvent, render, waitFor } from '../../../tests/utils';
@ -233,7 +234,8 @@ describe('Typography copy', () => {
fireEvent.click(wrapper.querySelectorAll('.ant-typography-copy')[0]); fireEvent.click(wrapper.querySelectorAll('.ant-typography-copy')[0]);
}); });
it('copy to clipboard', done => { it('copy to clipboard', () => {
jest.useFakeTimers();
const spy = jest.spyOn(copyObj, 'default'); const spy = jest.spyOn(copyObj, 'default');
const originText = 'origin text.'; const originText = 'origin text.';
const nextText = 'next text.'; const nextText = 'next text.';
@ -243,7 +245,7 @@ describe('Typography copy', () => {
setTimeout(() => { setTimeout(() => {
setDynamicText(nextText); setDynamicText(nextText);
}, 500); }, 500);
}); }, []);
return ( return (
<Base component="p" copyable> <Base component="p" copyable>
{dynamicText} {dynamicText}
@ -254,12 +256,13 @@ describe('Typography copy', () => {
const copyBtn = wrapper.querySelectorAll('.ant-typography-copy')[0]; const copyBtn = wrapper.querySelectorAll('.ant-typography-copy')[0];
fireEvent.click(copyBtn); fireEvent.click(copyBtn);
expect(spy.mock.calls[0][0]).toEqual(originText); expect(spy.mock.calls[0][0]).toEqual(originText);
setTimeout(() => { act(() => {
spy.mockReset(); jest.runAllTimers();
fireEvent.click(copyBtn); });
expect(spy.mock.calls[0][0]).toEqual(nextText); spy.mockReset();
done(); fireEvent.click(copyBtn);
}, 500); expect(spy.mock.calls[0][0]).toEqual(nextText);
jest.useRealTimers();
}); });
}); });
}); });

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import Upload from './Upload';
import type { UploadProps } from './interface'; import type { UploadProps } from './interface';
import Upload from './Upload';
export type DraggerProps = UploadProps & { height?: number }; export type DraggerProps = UploadProps & { height?: number };
@ -11,6 +11,8 @@ const InternalDragger: React.ForwardRefRenderFunction<unknown, DraggerProps> = (
const Dragger = React.forwardRef(InternalDragger) as React.FC<DraggerProps>; const Dragger = React.forwardRef(InternalDragger) as React.FC<DraggerProps>;
Dragger.displayName = 'Dragger'; if (process.env.NODE_ENV !== 'production') {
Dragger.displayName = 'Dragger';
}
export default Dragger; export default Dragger;

View File

@ -1,24 +1,25 @@
import * as React from 'react'; import classNames from 'classnames';
import type { UploadProps as RcUploadProps } from 'rc-upload'; import type { UploadProps as RcUploadProps } from 'rc-upload';
import RcUpload from 'rc-upload'; import RcUpload from 'rc-upload';
import useMergedState from 'rc-util/lib/hooks/useMergedState'; import useMergedState from 'rc-util/lib/hooks/useMergedState';
import classNames from 'classnames'; import * as React from 'react';
import UploadList from './UploadList'; import { ConfigContext } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import warning from '../_util/warning';
import type { import type {
RcFile, RcFile,
ShowUploadListInterface, ShowUploadListInterface,
UploadFile,
UploadLocale,
UploadChangeParam, UploadChangeParam,
UploadType, UploadFile,
UploadListType, UploadListType,
UploadLocale,
UploadType,
} from './interface'; } from './interface';
import { UploadProps } from './interface'; import { UploadProps } from './interface';
import UploadList from './UploadList';
import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils'; import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import { ConfigContext } from '../config-provider';
import warning from '../_util/warning';
import useStyle from './style'; import useStyle from './style';
export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`; export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`;
@ -435,8 +436,9 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
}; };
const Upload = React.forwardRef<unknown, UploadProps>(InternalUpload); const Upload = React.forwardRef<unknown, UploadProps>(InternalUpload);
if (process.env.NODE_ENV !== 'production') {
Upload.displayName = 'Upload'; Upload.displayName = 'Upload';
}
Upload.defaultProps = { Upload.defaultProps = {
type: 'select' as UploadType, type: 'select' as UploadType,

View File

@ -1,20 +1,20 @@
import * as React from 'react'; import FileTwoTone from '@ant-design/icons/FileTwoTone';
import type { CSSMotionListProps } from 'rc-motion';
import CSSMotion, { CSSMotionList } from 'rc-motion';
import classNames from 'classnames';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined'; import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined';
import PictureTwoTone from '@ant-design/icons/PictureTwoTone'; import PictureTwoTone from '@ant-design/icons/PictureTwoTone';
import FileTwoTone from '@ant-design/icons/FileTwoTone'; import classNames from 'classnames';
import { cloneElement, isValidElement } from '../../_util/reactNode'; import type { CSSMotionListProps } from 'rc-motion';
import { previewImage, isImageUrl } from '../utils'; import CSSMotion, { CSSMotionList } from 'rc-motion';
import collapseMotion from '../../_util/motion'; import * as React from 'react';
import { ConfigContext } from '../../config-provider';
import type { ButtonProps } from '../../button'; import type { ButtonProps } from '../../button';
import Button from '../../button'; import Button from '../../button';
import { ConfigContext } from '../../config-provider';
import useForceUpdate from '../../_util/hooks/useForceUpdate'; import useForceUpdate from '../../_util/hooks/useForceUpdate';
import collapseMotion from '../../_util/motion';
import { cloneElement, isValidElement } from '../../_util/reactNode';
import type { InternalUploadFile, UploadFile, UploadListProps, UploadListType } from '../interface';
import { isImageUrl, previewImage } from '../utils';
import ListItem from './ListItem'; import ListItem from './ListItem';
import type { UploadListProps, UploadFile, UploadListType, InternalUploadFile } from '../interface';
const listItemMotion: Partial<CSSMotionListProps> = { const listItemMotion: Partial<CSSMotionListProps> = {
...collapseMotion, ...collapseMotion,
@ -246,8 +246,9 @@ const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProp
}; };
const UploadList = React.forwardRef<unknown, UploadListProps>(InternalUploadList); const UploadList = React.forwardRef<unknown, UploadListProps>(InternalUploadList);
if (process.env.NODE_ENV !== 'production') {
UploadList.displayName = 'UploadList'; UploadList.displayName = 'UploadList';
}
UploadList.defaultProps = { UploadList.defaultProps = {
listType: 'text' as UploadListType, // or picture listType: 'text' as UploadListType, // or picture

View File

@ -28,8 +28,8 @@ exports[`Upload List handle error 1`] = `
class="ant-upload-list ant-upload-list-text" class="ant-upload-list ant-upload-list-text"
> >
<div <div
class="ant-upload-list-item-container ant-upload-animate-appear ant-upload-animate-appear-active ant-upload-animate" class="ant-upload-list-item-container"
style="height: 0px; opacity: 1;" style=""
> >
<div <div
class="ant-upload-list-item ant-upload-list-item-error" class="ant-upload-list-item ant-upload-list-item-error"

View File

@ -1,10 +1,10 @@
/* eslint-disable react/no-string-refs, react/prefer-es6-class */ /* eslint-disable react/no-string-refs, react/prefer-es6-class */
import React from 'react'; import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import Upload from '..'; import Upload from '..';
import { setup, teardown } from './mock';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import { fireEvent, render, waitFor } from '../../../tests/utils';
import { setup, teardown } from './mock';
describe('Upload.Dragger', () => { describe('Upload.Dragger', () => {
mountTest(Upload.Dragger); mountTest(Upload.Dragger);
@ -12,44 +12,47 @@ describe('Upload.Dragger', () => {
beforeEach(() => setup()); beforeEach(() => setup());
afterEach(() => teardown()); afterEach(() => teardown());
it('support drag file with over style', () => { it('support drag file with over style', async () => {
jest.useFakeTimers(); jest.useFakeTimers();
const wrapper = mount( const { container: wrapper } = render(
<Upload.Dragger action="http://upload.com"> <Upload.Dragger action="http://upload.com">
<div /> <div />
</Upload.Dragger>, </Upload.Dragger>,
); );
wrapper.find('.ant-upload-drag-container').simulate('dragover', { fireEvent.dragOver(wrapper.querySelector('.ant-upload-drag-container'), {
target: { target: {
files: [{ file: 'foo.png' }], files: [{ file: 'foo.png' }],
}, },
}); });
act(() => { await act(() => {
jest.runAllTimers(); jest.runAllTimers();
}); });
wrapper.update();
expect(wrapper.find('.ant-upload-drag').hasClass('ant-upload-drag-hover')).toBe(true); await waitFor(() => {
expect(wrapper.querySelector('.ant-upload-drag')).toHaveClass('ant-upload-drag-hover');
});
jest.useRealTimers(); jest.useRealTimers();
}); });
it('support onDrop when files are dropped onto upload area', () => { it('support onDrop when files are dropped onto upload area', async () => {
const onDrop = jest.fn(); const onDrop = jest.fn();
const wrapper = mount( const { container: wrapper } = render(
<Upload.Dragger onDrop={onDrop}> <Upload.Dragger onDrop={onDrop}>
<div /> <div />
</Upload.Dragger>, </Upload.Dragger>,
); );
wrapper.find('.ant-upload-drag-container').simulate('drop', { fireEvent.drop(wrapper.querySelector('.ant-upload-drag-container'), {
dataTransfer: { dataTransfer: {
files: [new File(['foo'], 'foo.png', { type: 'image/png' })], files: [new File(['foo'], 'foo.png', { type: 'image/png' })],
}, },
}); });
expect(onDrop).toHaveBeenCalled(); await waitFor(() => {
expect(onDrop).toHaveBeenCalled();
});
}); });
}); });

View File

@ -1,17 +1,16 @@
/* eslint-disable react/no-string-refs, react/prefer-es6-class */ /* eslint-disable react/no-string-refs, react/prefer-es6-class */
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import produce from 'immer'; import produce from 'immer';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React from 'react';
import { act } from 'react-dom/test-utils';
import Upload from '..'; import Upload from '..';
import Form from '../../form';
import { getFileItem, removeFileItem, isImageUrl } from '../utils';
import { setup, teardown } from './mock';
import { resetWarned } from '../../_util/warning';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { sleep, render, fireEvent } from '../../../tests/utils'; import { fireEvent, render, sleep } from '../../../tests/utils';
import Form from '../../form';
import { resetWarned } from '../../_util/warning';
import { getFileItem, isImageUrl, removeFileItem } from '../utils';
import { setup, teardown } from './mock';
globalThis.IS_REACT_ACT_ENVIRONMENT = true; globalThis.IS_REACT_ACT_ENVIRONMENT = true;
@ -46,12 +45,14 @@ describe('Upload', () => {
); );
} }
} }
mount(<App />); render(<App />);
expect(ref).toBeDefined(); expect(ref).toBeDefined();
}); });
it('return promise in beforeUpload', done => { it('return promise in beforeUpload', async () => {
jest.useFakeTimers();
const data = jest.fn(); const data = jest.fn();
const done = jest.fn();
const props = { const props = {
action: 'http://upload.com', action: 'http://upload.com',
beforeUpload: () => beforeUpload: () =>
@ -67,19 +68,33 @@ describe('Upload', () => {
}, },
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [{ file: 'foo.png' }], files: [{ file: 'foo.png' }],
}, },
}); });
act(() => {
jest.runAllTimers();
});
await act(async () => {
for (let i = 0; i < 4; i += 1) {
// eslint-disable-next-line no-await-in-loop
await Promise.resolve();
}
});
expect(done).toHaveBeenCalled();
jest.useRealTimers();
}); });
it('beforeUpload can be falsy', done => { it('beforeUpload can be falsy', async () => {
jest.useFakeTimers();
const done = jest.fn();
const props = { const props = {
action: 'http://upload.com', action: 'http://upload.com',
beforeUpload: false, beforeUpload: false,
@ -90,20 +105,30 @@ describe('Upload', () => {
}, },
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [{ file: 'foo.png' }], files: [{ file: 'foo.png' }],
}, },
}); });
await act(async () => {
for (let i = 0; i < 4; i += 1) {
// eslint-disable-next-line no-await-in-loop
await Promise.resolve();
}
});
expect(done).toHaveBeenCalled();
jest.useRealTimers();
}); });
it('upload promise return file in beforeUpload', done => { it('upload promise return file in beforeUpload', async () => {
jest.useFakeTimers();
const done = jest.fn();
const data = jest.fn(); const data = jest.fn();
const props = { const props = {
action: 'http://upload.com', action: 'http://upload.com',
@ -125,17 +150,29 @@ describe('Upload', () => {
}, },
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [{ file: 'foo.png' }], files: [{ file: 'foo.png' }],
}, },
}); });
act(() => {
jest.runAllTimers();
});
await act(async () => {
for (let i = 0; i < 4; i += 1) {
// eslint-disable-next-line no-await-in-loop
await Promise.resolve();
}
});
expect(done).toHaveBeenCalled();
jest.useRealTimers();
}); });
it('should not stop upload when return value of beforeUpload is false', done => { it('should not stop upload when return value of beforeUpload is false', done => {
@ -162,13 +199,13 @@ describe('Upload', () => {
}, },
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [mockFile], files: [mockFile],
}, },
@ -187,13 +224,13 @@ describe('Upload', () => {
}, },
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [{ file: 'foo.png' }], files: [{ file: 'foo.png' }],
}, },
@ -247,10 +284,10 @@ describe('Upload', () => {
</Form> </Form>
); );
const wrapper = mount(<Demo />); const { container: wrapper, rerender } = render(<Demo />);
expect(wrapper.find('input#upload').length).toBe(1); expect(wrapper.querySelectorAll('input#upload').length).toBe(1);
wrapper.setProps({ disabled: true }); rerender(<Demo disabled />);
expect(wrapper.find('input#upload').length).toBe(0); expect(wrapper.querySelectorAll('input#upload').length).toBe(0);
}); });
// https://github.com/ant-design/ant-design/issues/24197 // https://github.com/ant-design/ant-design/issues/24197
@ -265,10 +302,10 @@ describe('Upload', () => {
</Form> </Form>
); );
const wrapper = mount(<Demo />); const { container: wrapper, rerender } = render(<Demo />);
expect(wrapper.find('input#upload').length).toBe(1); expect(wrapper.querySelectorAll('input#upload').length).toBe(1);
wrapper.setProps({ disabled: true }); rerender(<Demo disabled />);
expect(wrapper.find('input#upload').length).toBe(0); expect(wrapper.querySelectorAll('input#upload').length).toBe(0);
}); });
it('should be controlled by fileList', () => { it('should be controlled by fileList', () => {
@ -282,10 +319,9 @@ describe('Upload', () => {
}, },
]; ];
const ref = React.createRef(); const ref = React.createRef();
const wrapper = mount(<Upload ref={ref} />); const { rerender } = render(<Upload ref={ref} />);
expect(ref.current.fileList).toEqual([]); expect(ref.current.fileList).toEqual([]);
rerender(<Upload ref={ref} fileList={fileList} />);
wrapper.setProps({ fileList });
jest.runAllTimers(); jest.runAllTimers();
expect(ref.current.fileList).toEqual(fileList); expect(ref.current.fileList).toEqual(fileList);
jest.useRealTimers(); jest.useRealTimers();
@ -299,7 +335,7 @@ describe('Upload', () => {
url: 'http://www.baidu.com/xxx.png', url: 'http://www.baidu.com/xxx.png',
}, },
]; ];
mount(<Upload fileList={fileList} />); render(<Upload fileList={fileList} />);
fileList.forEach(file => { fileList.forEach(file => {
expect(file.uid).toBeDefined(); expect(file.uid).toBeDefined();
}); });
@ -395,10 +431,10 @@ describe('Upload', () => {
}, },
}, },
]; ];
const wrapper = mount(<Upload fileList={fileList} />); const { container: wrapper } = render(<Upload fileList={fileList} />);
const linkNode = wrapper.find('a.ant-upload-list-item-name'); const linkNode = wrapper.querySelector('a.ant-upload-list-item-name');
expect(linkNode.props().download).toBe('image'); expect(linkNode.getAttribute('download')).toBe('image');
expect(linkNode.props().rel).toBe('noopener'); expect(linkNode.getAttribute('rel')).toBe('noopener');
}); });
it('should support linkProps as json stringify', () => { it('should support linkProps as json stringify', () => {
@ -415,10 +451,10 @@ describe('Upload', () => {
linkProps: linkPropsString, linkProps: linkPropsString,
}, },
]; ];
const wrapper = mount(<Upload fileList={fileList} />); const { container: wrapper } = render(<Upload fileList={fileList} />);
const linkNode = wrapper.find('a.ant-upload-list-item-name'); const linkNode = wrapper.querySelector('a.ant-upload-list-item-name');
expect(linkNode.props().download).toBe('image'); expect(linkNode.getAttribute('download')).toBe('image');
expect(linkNode.props().rel).toBe('noopener'); expect(linkNode.getAttribute('rel')).toBe('noopener');
}); });
it('should not stop remove when return value of onRemove is false', done => { it('should not stop remove when return value of onRemove is false', done => {
@ -435,13 +471,11 @@ describe('Upload', () => {
], ],
}; };
const wrapper = mount(<Upload {...props} />); const { container: wrapper } = render(<Upload {...props} />);
wrapper.find('div.ant-upload-list-item .anticon-delete').simulate('click'); fireEvent.click(wrapper.querySelector('div.ant-upload-list-item .anticon-delete'));
setTimeout(() => { setTimeout(() => {
wrapper.update();
expect(mockRemove).toHaveBeenCalled(); expect(mockRemove).toHaveBeenCalled();
expect(props.fileList).toHaveLength(1); expect(props.fileList).toHaveLength(1);
expect(props.fileList[0].status).toBe('done'); expect(props.fileList[0].status).toBe('done');
@ -504,13 +538,11 @@ describe('Upload', () => {
], ],
}; };
const wrapper = mount(<Upload {...props} onDownload={() => {}} />); const { container: wrapper } = render(<Upload {...props} onDownload={() => {}} />);
wrapper.find('div.ant-upload-list-item .anticon-download').simulate('click'); fireEvent.click(wrapper.querySelector('div.ant-upload-list-item .anticon-download'));
setTimeout(() => { setTimeout(() => {
wrapper.update();
expect(props.fileList).toHaveLength(1); expect(props.fileList).toHaveLength(1);
expect(props.fileList[0].status).toBe('done'); expect(props.fileList[0].status).toBe('done');
done(); done();
@ -520,7 +552,7 @@ describe('Upload', () => {
// https://github.com/ant-design/ant-design/issues/14439 // https://github.com/ant-design/ant-design/issues/14439
it('should allow call abort function through upload instance', () => { it('should allow call abort function through upload instance', () => {
const ref = React.createRef(); const ref = React.createRef();
mount( render(
<Upload ref={ref}> <Upload ref={ref}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
@ -530,18 +562,18 @@ describe('Upload', () => {
it('correct dragCls when type is drag', () => { it('correct dragCls when type is drag', () => {
const fileList = [{ status: 'uploading', uid: 'file' }]; const fileList = [{ status: 'uploading', uid: 'file' }];
const wrapper = mount( const { container: wrapper } = render(
<Upload type="drag" fileList={fileList}> <Upload type="drag" fileList={fileList}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
expect(wrapper.find('.ant-upload-drag-uploading').length).toBe(1); expect(wrapper.querySelectorAll('.ant-upload-drag-uploading').length).toBe(1);
}); });
it('return when targetItem is null', () => { it('return when targetItem is null', () => {
const fileList = [{ uid: 'file' }]; const fileList = [{ uid: 'file' }];
const ref = React.createRef(); const ref = React.createRef();
mount( render(
<Upload ref={ref} type="drag" fileList={fileList}> <Upload ref={ref} type="drag" fileList={fileList}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
@ -554,7 +586,7 @@ describe('Upload', () => {
it('should replace file when targetItem already exists', () => { it('should replace file when targetItem already exists', () => {
const fileList = [{ uid: 'file', name: 'file' }]; const fileList = [{ uid: 'file', name: 'file' }];
const ref = React.createRef(); const ref = React.createRef();
const wrapper = mount( const { unmount } = render(
<Upload ref={ref} defaultFileList={fileList}> <Upload ref={ref} defaultFileList={fileList}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
@ -574,22 +606,20 @@ describe('Upload', () => {
]); ]);
}); });
wrapper.update();
expect(ref.current.fileList.length).toBe(1); expect(ref.current.fileList.length).toBe(1);
expect(ref.current.fileList[0].originFileObj).toEqual({ expect(ref.current.fileList[0].originFileObj).toEqual({
name: 'file1', name: 'file1',
uid: 'file', uid: 'file',
}); });
wrapper.unmount(); unmount();
}); });
it('warning if set `value`', () => { it('warning if set `value`', () => {
resetWarned(); resetWarned();
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Upload value={[]} />); render(<Upload value={[]} />);
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Upload] `value` is not a valid prop, do you mean `fileList`?', 'Warning: [antd: Upload] `value` is not a valid prop, do you mean `fileList`?',
); );
@ -603,8 +633,8 @@ describe('Upload', () => {
type: 'video/mp4', type: 'video/mp4',
url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
}; };
const wrapper = mount(<Upload listType="picture-card" fileList={[file]} />); const { container: wrapper } = render(<Upload listType="picture-card" fileList={[file]} />);
expect(wrapper.find('img').length).toBe(0); expect(wrapper.querySelectorAll('img').length).toBe(0);
}); });
// https://github.com/ant-design/ant-design/issues/25077 // https://github.com/ant-design/ant-design/issues/25077
@ -612,24 +642,29 @@ describe('Upload', () => {
const onClick = jest.fn(); const onClick = jest.fn();
const onMouseEnter = jest.fn(); const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn(); const onMouseLeave = jest.fn();
const wrapper = mount( const { container: wrapper } = render(
<Upload onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <Upload onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('.ant-upload').at(1).simulate('click'); fireEvent.click(wrapper.querySelectorAll('.ant-upload')[1]);
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
wrapper.find('.ant-upload').at(1).simulate('mouseEnter'); fireEvent.mouseEnter(wrapper.querySelectorAll('.ant-upload')[1]);
expect(onMouseEnter).toHaveBeenCalled(); expect(onMouseEnter).toHaveBeenCalled();
wrapper.find('.ant-upload').at(1).simulate('mouseLeave'); fireEvent.mouseLeave(wrapper.querySelectorAll('.ant-upload')[1]);
expect(onMouseLeave).toHaveBeenCalled(); expect(onMouseLeave).toHaveBeenCalled();
}); });
// https://github.com/ant-design/ant-design/issues/26427 // https://github.com/ant-design/ant-design/issues/26427
it('should sync file list with control mode', done => { it('should sync file list with control mode', async () => {
jest.useFakeTimers();
const done = jest.fn();
let callTimes = 0; let callTimes = 0;
const customRequest = jest.fn(async options => { const customRequest = jest.fn(async options => {
// stop here to make sure new fileList has been set and passed to Upload
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 0));
options.onProgress({ percent: 0 }); options.onProgress({ percent: 0 });
const url = Promise.resolve('https://ant.design'); const url = Promise.resolve('https://ant.design');
options.onProgress({ percent: 100 }); options.onProgress({ percent: 100 });
@ -639,7 +674,7 @@ describe('Upload', () => {
const Demo = () => { const Demo = () => {
const [fileList, setFileList] = React.useState([]); const [fileList, setFileList] = React.useState([]);
const onChange = e => { const onChange = async e => {
const newFileList = Array.isArray(e) ? e : e.fileList; const newFileList = Array.isArray(e) ? e : e.fileList;
setFileList(newFileList); setFileList(newFileList);
const file = newFileList[0]; const file = newFileList[0];
@ -676,15 +711,29 @@ describe('Upload', () => {
); );
}; };
const wrapper = mount(<Demo />); const { container: wrapper } = render(<Demo />);
act(() => { fireEvent.change(wrapper.querySelector('input'), {
wrapper.find('input').simulate('change', { target: {
target: { files: [{ file: 'foo.png' }],
files: [{ file: 'foo.png' }], },
},
});
}); });
await act(async () => {
for (let i = 0; i < 3; i += 1) {
// eslint-disable-next-line no-await-in-loop
await Promise.resolve();
}
});
await act(() => {
jest.runAllTimers();
});
await act(async () => {
await Promise.resolve();
});
expect(done).toHaveBeenCalled();
jest.useRealTimers();
}); });
describe('maxCount', () => { describe('maxCount', () => {
@ -704,13 +753,13 @@ describe('Upload', () => {
maxCount: 1, maxCount: 1,
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [ files: [
new File(['foo'], 'foo.png', { new File(['foo'], 'foo.png', {
@ -746,13 +795,13 @@ describe('Upload', () => {
maxCount: 2, maxCount: 2,
}; };
const wrapper = mount( const { container: wrapper } = render(
<Upload {...props}> <Upload {...props}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [ files: [
new File(['foo'], 'foo.png', { new File(['foo'], 'foo.png', {
@ -788,7 +837,7 @@ describe('Upload', () => {
expect(fileList[0].uid).toBeFalsy(); expect(fileList[0].uid).toBeFalsy();
mount( render(
<Upload fileList={fileList}> <Upload fileList={fileList}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
@ -800,13 +849,13 @@ describe('Upload', () => {
it('Proxy should support deepClone', async () => { it('Proxy should support deepClone', async () => {
const onChange = jest.fn(); const onChange = jest.fn();
const wrapper = mount( const { container: wrapper } = render(
<Upload onChange={onChange}> <Upload onChange={onChange}>
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
wrapper.find('input').simulate('change', { fireEvent.change(wrapper.querySelector('input'), {
target: { target: {
files: [ files: [
new File(['foo'], 'foo.png', { new File(['foo'], 'foo.png', {
@ -843,9 +892,9 @@ describe('Upload', () => {
const frozenFileList = fileList.map(file => Object.freeze(file)); const frozenFileList = fileList.map(file => Object.freeze(file));
const wrapper = mount(<Upload fileList={frozenFileList} />); const { container: wrapper } = render(<Upload fileList={frozenFileList} />);
const rmBtn = wrapper.find('.ant-upload-list-item-action').last(); const rmBtn = wrapper.querySelectorAll('.ant-upload-list-item-card-actions-btn');
rmBtn.simulate('click'); fireEvent.click(rmBtn[rmBtn.length - 1]);
// Wait for Upload async remove // Wait for Upload async remove
await act(async () => { await act(async () => {
@ -893,7 +942,7 @@ describe('Upload', () => {
}); });
// Motion leave status change: start > active // Motion leave status change: start > active
act(() => { await act(() => {
jest.runAllTimers(); jest.runAllTimers();
}); });
@ -907,10 +956,10 @@ describe('Upload', () => {
}); });
it('<Upload /> should pass <UploadList /> prefixCls', async () => { it('<Upload /> should pass <UploadList /> prefixCls', async () => {
const wrapper1 = mount(<Upload />); const { container: wrapper } = render(<Upload />);
expect(wrapper1.find('.ant-upload-list').exists()).toBeTruthy(); expect(wrapper.querySelectorAll('.ant-upload-list').length).toBeGreaterThan(0);
const wrapper2 = mount(<Upload prefixCls="custom-upload" />); const { container: wrapper2 } = render(<Upload prefixCls="custom-upload" />);
expect(wrapper2.find('.custom-upload-list').exists()).toBeTruthy(); expect(wrapper2.querySelectorAll('.custom-upload-list').length).toBeGreaterThan(0);
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,8 @@ import Dragger from './Dragger';
import type { UploadProps } from './Upload'; import type { UploadProps } from './Upload';
import InternalUpload, { LIST_IGNORE } from './Upload'; import InternalUpload, { LIST_IGNORE } from './Upload';
export { UploadProps, UploadListProps, UploadChangeParam, RcFile } from './interface';
export { DraggerProps } from './Dragger'; export { DraggerProps } from './Dragger';
export { RcFile, UploadChangeParam, UploadFile, UploadListProps, UploadProps } from './interface';
type InternalUploadType = typeof InternalUpload; type InternalUploadType = typeof InternalUpload;
interface UploadInterface<T = any> extends InternalUploadType { interface UploadInterface<T = any> extends InternalUploadType {

View File

@ -1,6 +1,6 @@
{ {
"name": "antd", "name": "antd",
"version": "4.21.2", "version": "4.21.3",
"description": "An enterprise-class UI design language and React components implementation", "description": "An enterprise-class UI design language and React components implementation",
"title": "Ant Design", "title": "Ant Design",
"keywords": [ "keywords": [
@ -157,7 +157,7 @@
"rc-tree-select": "~5.4.0", "rc-tree-select": "~5.4.0",
"rc-trigger": "^5.2.10", "rc-trigger": "^5.2.10",
"rc-upload": "~4.3.0", "rc-upload": "~4.3.0",
"rc-util": "^5.21.3", "rc-util": "^5.22.3",
"scroll-into-view-if-needed": "^2.2.25", "scroll-into-view-if-needed": "^2.2.25",
"shallowequal": "^1.1.0" "shallowequal": "^1.1.0"
}, },
@ -236,7 +236,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"immer": "^9.0.1", "immer": "^9.0.1",
"immutability-helper": "^3.0.0", "immutability-helper": "^3.0.0",
"inquirer": "^8.0.0", "inquirer": "^9.0.0",
"intersection-observer": "^0.12.0", "intersection-observer": "^0.12.0",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jest": "^28.0.3", "jest": "^28.0.3",
@ -246,7 +246,7 @@
"jest-image-snapshot": "^5.1.0", "jest-image-snapshot": "^5.1.0",
"jest-puppeteer": "^6.0.0", "jest-puppeteer": "^6.0.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"jsdom": "^19.0.0", "jsdom": "^20.0.0",
"jsonml.js": "^0.1.0", "jsonml.js": "^0.1.0",
"less-vars-to-js": "^1.3.0", "less-vars-to-js": "^1.3.0",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",