Merge branch 'master' into next-merge-master

This commit is contained in:
MadCcc 2022-06-16 10:46:55 +08:00
commit 9dd5e9b5e9
32 changed files with 846 additions and 588 deletions

View File

@ -37,7 +37,8 @@
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"selector-class-pattern": null,
"selector-id-pattern": null
"selector-id-pattern": null,
"selector-not-notation": null
},
"ignoreFiles": ["components/style/color/{bezierEasing,colorPalette,tinyColor}.less"]
}

View File

@ -15,6 +15,28 @@ timeline: true
---
## 4.21.2
`2022-06-15`
- 🐞 Fix incorrect Form status with `noStyle`. [#36054](https://github.com/ant-design/ant-design/pull/36054)
## 4.21.1
`2022-06-13`
- 🐞 Fixed Image the `getContainer` property not reading from ConfigProvider. [#36002](https://github.com/ant-design/ant-design/pull/36002) [@robothot](https://github.com/robothot)
- 🐞 Fixed Button issue [#35952](https://github.com/ant-design/ant-design/issues/35952) where the `disabled` attribute does not take effect with `href`. [#35975](https://github.com/ant-design/ant-design/pull/35975) [@MuxinFeng](https://github.com/MuxinFeng)
- 🐞 Fix less color palette algorithm according to `@ant-design/colors`. [#35954](https://github.com/ant-design/ant-design/pull/35954) [@christian-lechner](https://github.com/christian-lechner)
- 🐞 Fix Upload image flickering. [#35943](https://github.com/ant-design/ant-design/pull/35943)
- 💄 Remove styles from Form such as `status` for children of Modal and Drawer. [#35849](https://github.com/ant-design/ant-design/pull/35849)
- TypeScript
- 🤖 Fix type definition for `autoFocus` in Dropdown. [#35990](https://github.com/ant-design/ant-design/pull/35990) [@robothot](https://github.com/robothot)
- 🤖 Fix type definition for `MenuItemGroupType` in Menu. [#35790](https://github.com/ant-design/ant-design/pull/35790) [@MasaoBlue](https://github.com/MasaoBlue)
- 🤖 Fix Carousel type definition in React 18. [#35959](https://github.com/ant-design/ant-design/pull/35959)
- 🌐 Localization
- 🇮🇹 Fix italian translation for `Table.cancelSort` key. [#35970](https://github.com/ant-design/ant-design/pull/35970) [@gariggio](https://github.com/gariggio)
## 4.21.0
`2022-06-06`

View File

@ -15,6 +15,28 @@ timeline: true
---
## 4.21.2
`2022-06-15`
- 🐞 修复 Form 有 `noStyle` 属性时校验状态错误的问题。[#36054](https://github.com/ant-design/ant-design/pull/36054)
## 4.21.1
`2022-06-13`
- 🐞 修复 Image `getContainer` 属性没有从 ConfigProvider 中读取的问题。[#36002](https://github.com/ant-design/ant-design/pull/36002) [@robothot](https://github.com/robothot)
- 🐞 修复 Button 有 `href``disabled` 属性不生效的问题。[#35952](https://github.com/ant-design/ant-design/issues/35952)。[#35975](https://github.com/ant-design/ant-design/pull/35975) [@MuxinFeng](https://github.com/MuxinFeng)
- 🐞 修复 Upload 组件动画闪烁的问题。[#35943](https://github.com/ant-design/ant-design/pull/35943)
- 🐞 修复 less 色彩算法,使其和 `@ant-design/colors` 保持一致。[#35954](https://github.com/ant-design/ant-design/pull/35954) [@christian-lechner](https://github.com/christian-lechner)
- 💄 Form.Item 中的 Modal 或 Drawer 组件包含的控件去除 `status` 等受 Form 影响的样式。[#35849](https://github.com/ant-design/ant-design/pull/35849)
- TypeScript
- 🤖 修复 Dropdown `autoFocus` 属性定义。[#35990](https://github.com/ant-design/ant-design/pull/35990) [@robothot](https://github.com/robothot)
- 🤖 修复 Menu 中 `MenuItemGroupType` 的类型定义。[#35790](https://github.com/ant-design/ant-design/pull/35790) [@MasaoBlue](https://github.com/MasaoBlue)
- 🤖 修复 Carousel 在 React 18 下的 TS 定义问题。[#35959](https://github.com/ant-design/ant-design/pull/35959)
- 🌐 国际化
- 🇮🇹 修复 `Table.cancelSort` 的意大利语翻译。[#35970](https://github.com/ant-design/ant-design/pull/35970) [@gariggio](https://github.com/gariggio)
## 4.21.0
`2022-06-06`

View File

@ -41,7 +41,7 @@ The differences with Select are:
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| value | Selected option | string | - | |
| onBlur | Called when leaving the component | function() | - | |
| onChange | Called when select an option or input value change, or value of input is changed | function(value) | - | |
| onChange | Called when selecting an option or changing an input value | function(value) | - | |
| onDropdownVisibleChange | Call when dropdown open | function(open) | - | |
| onFocus | Called when entering the component | function() | - | |
| onSearch | Called when searching items | function(value) | - | |

View File

@ -1,11 +1,10 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { fireEvent, render } from '@testing-library/react';
import Badge from '../index';
import Tooltip from '../../tooltip';
import React from 'react';
import { act } from 'react-dom/test-utils';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import Tooltip from '../../tooltip';
import Badge from '../index';
describe('Badge', () => {
mountTest(Badge);
@ -27,34 +26,37 @@ describe('Badge', () => {
});
it('badge dot not scaling count > 9', () => {
const badge = mount(<Badge count={10} dot />);
expect(badge.find('.ant-card-multiple-words').length).toBe(0);
const { container } = render(<Badge count={10} dot />);
expect(container.querySelectorAll('.ant-card-multiple-words').length).toBe(0);
});
it('badge should support float number', () => {
let wrapper = mount(<Badge count={3.5} />);
expect(wrapper.find('.ant-badge-multiple-words').first().text()).toEqual('3.5');
const { container } = render(<Badge count={3.5} />);
expect(container.querySelectorAll('.ant-badge-multiple-words')[0].textContent).toEqual('3.5');
wrapper = mount(<Badge count="3.5" />);
expect(wrapper.find('.ant-badge-multiple-words').first().text()).toEqual('3.5');
expect(() => wrapper.unmount()).not.toThrow();
const { container: anotherContainer, unmount } = render(<Badge count="3.5" />);
expect(anotherContainer.querySelectorAll('.ant-badge-multiple-words')[0].textContent).toEqual(
'3.5',
);
expect(() => unmount()).not.toThrow();
});
it('badge dot not showing count == 0', () => {
const badge = mount(<Badge count={0} dot />);
expect(badge.find('.ant-badge-dot').length).toBe(0);
const { container } = render(<Badge count={0} dot />);
expect(container.querySelectorAll('.ant-badge-dot').length).toBe(0);
});
it('should have an overriden title attribute', () => {
const badge = mount(<Badge count={10} title="Custom title" />);
expect(
badge.find('.ant-scroll-number').getDOMNode().attributes.getNamedItem('title').value,
).toEqual('Custom title');
const { container } = render(<Badge count={10} title="Custom title" />);
expect((container.querySelector('.ant-scroll-number')! as HTMLElement).title).toEqual(
'Custom title',
);
});
// https://github.com/ant-design/ant-design/issues/10626
it('should be composable with Tooltip', () => {
const ref = React.createRef();
const ref = React.createRef<typeof Tooltip>();
const { container } = render(
<Tooltip title="Fix the error" ref={ref}>
<Badge status="error" />
@ -62,22 +64,21 @@ describe('Badge', () => {
);
act(() => {
fireEvent.mouseEnter(container.querySelector('.ant-badge'));
fireEvent.mouseEnter(container.querySelector('.ant-badge')!);
jest.runAllTimers();
});
expect(ref.current.props.visible).toBeTruthy();
expect((container.firstChild! as HTMLElement).classList).toContain('ant-tooltip-open');
});
it('should render when count is changed', () => {
const wrapper = mount(<Badge count={9} />);
const { asFragment, rerender } = render(<Badge count={9} />);
function updateMatch(count) {
wrapper.setProps({ count });
function updateMatch(count: number) {
rerender(<Badge count={count} />);
act(() => {
jest.runAllTimers();
wrapper.update();
expect(wrapper.render()).toMatchSnapshot();
expect(asFragment().firstChild).toMatchSnapshot();
});
}
@ -90,48 +91,52 @@ describe('Badge', () => {
});
it('should be compatible with borderColor style', () => {
const wrapper = mount(
const { asFragment } = render(
<Badge
count={4}
style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }}
/>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(asFragment().firstChild).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/13694
it('should support offset when count is a ReactNode', () => {
const wrapper = mount(
const { asFragment } = render(
<Badge count={<span className="custom" style={{ color: '#f5222d' }} />} offset={[10, 20]}>
<a href="#" className="head-example">
head
</a>
</Badge>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(asFragment().firstChild).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/15349
it('should color style works on Badge', () => {
const wrapper = mount(<Badge style={{ color: 'red' }} status="success" text="Success" />);
expect(wrapper.find('.ant-badge-status-text').props().style.color).toBe('red');
const { container } = render(
<Badge style={{ color: 'red' }} status="success" text="Success" />,
);
expect((container.querySelector('.ant-badge-status-text')! as HTMLElement).style.color).toEqual(
'red',
);
});
// https://github.com/ant-design/ant-design/issues/15799
it('render correct with negative number', () => {
const wrapper = mount(
const { asFragment } = render(
<div>
<Badge count="-10" />
<Badge count={-10} />
</div>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(asFragment().firstChild).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/21331
// https://github.com/ant-design/ant-design/issues/31590
it('render Badge status/color when contains children', () => {
const wrapper = mount(
const { container, asFragment } = render(
<div>
<Badge count={5} status="success">
<a />
@ -144,20 +149,20 @@ describe('Badge', () => {
</Badge>
</div>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.find(Badge).at(0).find('.ant-scroll-number-only-unit').text()).toBe('5');
expect(wrapper.find(Badge).at(1).find('.ant-scroll-number-only-unit').text()).toBe('5');
expect(wrapper.find(Badge).at(2).find('.ant-scroll-number-only-unit').text()).toBe('5');
expect(asFragment().firstChild).toMatchSnapshot();
expect(container.querySelectorAll('.ant-scroll-number-only-unit')[0].textContent).toBe('5');
expect(container.querySelectorAll('.ant-scroll-number-only-unit')[1].textContent).toBe('5');
expect(container.querySelectorAll('.ant-scroll-number-only-unit')[2].textContent).toBe('5');
});
it('Badge should work when status/color is empty string', () => {
const wrapper = mount(
const { container } = render(
<>
<Badge color="" text="text" />
<Badge status="" text="text" />
<Badge status={'' as any} text="text" />
</>,
);
expect(wrapper.find('.ant-badge')).toHaveLength(2);
expect(container.querySelectorAll('.ant-badge')).toHaveLength(2);
});
});

View File

@ -1,8 +1,8 @@
import { render } from '@testing-library/react';
import React from 'react';
import { mount } from 'enzyme';
import Badge from '../index';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import Badge from '../index';
describe('Ribbon', () => {
mountTest(Badge.Ribbon);
@ -10,65 +10,73 @@ describe('Ribbon', () => {
describe('placement', () => {
it('works with `start` & `end` placement', () => {
const wrapperStart = mount(
const { container: wrapperStart } = render(
<Badge.Ribbon placement="start">
<div />
</Badge.Ribbon>,
);
expect(wrapperStart.find('.ant-ribbon-placement-start').length).toEqual(1);
expect(wrapperStart.querySelectorAll('.ant-ribbon-placement-start').length).toEqual(1);
const wrapperEnd = mount(
const { container: wrapperEnd } = render(
<Badge.Ribbon placement="end">
<div />
</Badge.Ribbon>,
);
expect(wrapperEnd.find('.ant-ribbon-placement-end').length).toEqual(1);
expect(wrapperEnd.querySelectorAll('.ant-ribbon-placement-end').length).toEqual(1);
});
});
describe('color', () => {
it('works with preset color', () => {
const wrapper = mount(
const { container } = render(
<Badge.Ribbon color="green">
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.ant-ribbon-color-green').length).toEqual(1);
expect(container.querySelectorAll('.ant-ribbon-color-green').length).toEqual(1);
});
it('works with custom color', () => {
const wrapperLeft = mount(
const { container: wrapperLeft } = render(
<Badge.Ribbon color="#888" placement="start">
<div />
</Badge.Ribbon>,
);
expect(wrapperLeft.find('.ant-ribbon').prop('style')?.background).toEqual('#888');
expect(wrapperLeft.find('.ant-ribbon-corner').prop('style')?.color).toEqual('#888');
const wrapperRight = mount(
expect((wrapperLeft.querySelector('.ant-ribbon')! as HTMLElement).style.background).toEqual(
'rgb(136, 136, 136)',
);
expect((wrapperLeft.querySelector('.ant-ribbon-corner')! as HTMLElement).style.color).toEqual(
'rgb(136, 136, 136)',
);
const { container: wrapperRight } = render(
<Badge.Ribbon color="#888" placement="end">
<div />
</Badge.Ribbon>,
);
expect(wrapperRight.find('.ant-ribbon').prop('style')?.background).toEqual('#888');
expect(wrapperRight.find('.ant-ribbon-corner').prop('style')?.color).toEqual('#888');
expect((wrapperRight.querySelector('.ant-ribbon')! as HTMLElement).style.background).toEqual(
'rgb(136, 136, 136)',
);
expect(
(wrapperRight.querySelector('.ant-ribbon-corner')! as HTMLElement).style.color,
).toEqual('rgb(136, 136, 136)');
});
});
describe('text', () => {
it('works with string', () => {
const wrapper = mount(
const { container } = render(
<Badge.Ribbon text="cool">
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.ant-ribbon').text()).toEqual('cool');
expect(container.querySelector('.ant-ribbon')?.textContent).toEqual('cool');
});
it('works with element', () => {
const wrapper = mount(
const { container } = render(
<Badge.Ribbon text={<span className="cool" />}>
<div />
</Badge.Ribbon>,
);
expect(wrapper.find('.cool').length).toEqual(1);
expect(container.querySelectorAll('.cool').length).toEqual(1);
});
});
});

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { SearchOutlined } from '@ant-design/icons';
import { mount } from 'enzyme';
import { resetWarned } from 'rc-util/lib/warning';
import React, { Component } from 'react';
import { act } from 'react-dom/test-utils';
import Button from '..';
import ConfigProvider from '../../config-provider';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep, render, fireEvent } from '../../../tests/utils';
import { fireEvent, render, sleep } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import type { SizeType } from '../../config-provider/SizeContext';
describe('Button', () => {
@ -328,6 +328,15 @@ describe('Button', () => {
expect(onClick).not.toHaveBeenCalled();
});
it('should match class .ant-btn-disabled when button is disabled and href is not undefined', () => {
const wrapper = render(
<Button href="https://ant.design" disabled>
click me
</Button>,
);
expect(wrapper.container.querySelector('.ant-btn')).toHaveClass('ant-btn-disabled');
});
// https://github.com/ant-design/ant-design/issues/30953
it('should handle fragment as children', () => {
const wrapper = render(

View File

@ -278,6 +278,10 @@ a.@{btn-prefix-cls} {
padding-top: 0.01px !important;
line-height: @btn-height-base - 2px;
&-disabled {
.btn-href-disabled();
}
&-lg {
line-height: @btn-height-lg - 2px;
}

View File

@ -385,6 +385,24 @@
}
.button-disabled(@disabled-color; transparent; transparent);
}
// link button disabled style
.btn-href-disabled() {
cursor: not-allowed;
> * {
pointer-events: none;
}
&,
&:hover,
&:focus,
&:active {
.button-color(@btn-disable-color,transparent, transparent);
text-shadow: none;
box-shadow: none;
}
}
// text button style
.btn-text() {
.button-variant-other(@text-color, transparent, transparent);

View File

@ -1,18 +1,18 @@
/* eslint-disable react/button-has-type */
import * as React from 'react';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import Group, { GroupSizeContext } from './button-group';
import { ConfigContext } from '../config-provider';
import Wave from '../_util/wave';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import LoadingIcon from './LoadingIcon';
import { cloneElement } from '../_util/reactNode';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import Wave from '../_util/wave';
import Group, { GroupSizeContext } from './button-group';
import LoadingIcon from './LoadingIcon';
// CSSINJS
import useStyle from './style';
@ -254,6 +254,8 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
const iconType = innerLoading ? 'loading' : icon;
const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
const classes = classNames(
prefixCls,
hashId,
@ -268,6 +270,7 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-disabled`]: linkButtonRestProps.href !== undefined && mergedDisabled,
},
className,
);
@ -284,7 +287,6 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
: null;
const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']);
if (linkButtonRestProps.href !== undefined) {
return wrapSSR(
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>

View File

@ -1,15 +1,15 @@
import * as React from 'react';
import RcDropdown from 'rc-dropdown';
import classNames from 'classnames';
import RightOutlined from '@ant-design/icons/RightOutlined';
import DropdownButton from './dropdown-button';
import classNames from 'classnames';
import RcDropdown from 'rc-dropdown';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import warning from '../_util/warning';
import { tuple } from '../_util/type';
import { cloneElement } from '../_util/reactNode';
import getPlacements from '../_util/placements';
import OverrideContext from '../menu/OverrideContext';
import type { OverrideContextProps } from '../menu/OverrideContext';
import OverrideContext from '../menu/OverrideContext';
import getPlacements from '../_util/placements';
import { cloneElement } from '../_util/reactNode';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import DropdownButton from './dropdown-button';
import useStyle from './style';
const Placements = tuple(
@ -45,6 +45,7 @@ export type DropdownArrowOptions = {
};
export interface DropdownProps {
autoFocus?: boolean;
arrow?: boolean | DropdownArrowOptions;
trigger?: ('click' | 'hover' | 'contextMenu')[];
overlay: React.ReactElement | OverlayFunc;

View File

@ -27,8 +27,8 @@ import FormItemLabel from './FormItemLabel';
import useDebounce from './hooks/useDebounce';
import useFrameState from './hooks/useFrameState';
import useItemRef from './hooks/useItemRef';
import { getFieldId, toArray } from './util';
import useStyle from './style';
import { getFieldId, toArray } from './util';
const NAME_SPLIT = '__SPLIT__';
@ -221,14 +221,9 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
const getItemRef = useItemRef();
// ======================== Status ========================
const { status: contextStatus, hasFeedback: contextHasFeedback } =
useContext(FormItemInputContext);
let mergedValidateStatus: ValidateStatus | undefined;
let mergedValidateStatus: ValidateStatus = '';
if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus;
} else if (contextStatus !== undefined) {
mergedValidateStatus = contextStatus;
} else if (meta?.validating) {
mergedValidateStatus = 'validating';
} else if (debounceErrors.length) {
@ -239,11 +234,9 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
mergedValidateStatus = 'success';
}
const mergedHasFeedback = hasFeedback || contextHasFeedback;
const formItemStatusContext = useMemo<FormItemStatusContextProps>(() => {
let feedbackIcon: ReactNode;
if (mergedHasFeedback) {
if (hasFeedback) {
const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus];
feedbackIcon = IconNode ? (
<span
@ -259,19 +252,11 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
return {
status: mergedValidateStatus,
hasFeedback: mergedHasFeedback,
hasFeedback,
feedbackIcon,
isFormItemInput: true,
};
}, [mergedValidateStatus, mergedHasFeedback]);
const noOverrideFormItemContext = useMemo<FormItemStatusContextProps>(
() => ({
...formItemStatusContext,
isFormItemInput: false,
}),
[formItemStatusContext],
);
}, [mergedValidateStatus, hasFeedback]);
// ======================== Render ========================
function renderLayout(
@ -280,11 +265,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
isRequired?: boolean,
): React.ReactNode {
if (noStyle && !hidden) {
return (
<FormItemInputContext.Provider value={noOverrideFormItemContext}>
{baseChildren}
</FormItemInputContext.Provider>
);
return baseChildren;
}
const itemClassName = {
@ -294,7 +275,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
[`${className}`]: !!className,
// Status
[`${prefixCls}-item-has-feedback`]: mergedValidateStatus && mergedHasFeedback,
[`${prefixCls}-item-has-feedback`]: mergedValidateStatus && hasFeedback,
[`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success',
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',

View File

@ -6836,7 +6836,7 @@ exports[`renders ./components/form/demo/normal-login.md extend context correctly
class="ant-form-item-control-input-content"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-checkbox-wrapper-in-form-item"
>
<span
class="ant-checkbox ant-checkbox-checked"
@ -18031,7 +18031,7 @@ exports[`renders ./components/form/demo/validate-other.md extend context correct
class="ant-form-item-control-input-content"
>
<div
class="ant-input-number"
class="ant-input-number ant-input-number-in-form-item"
>
<div
class="ant-input-number-handler-wrap"

View File

@ -4451,7 +4451,7 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-checkbox-wrapper-in-form-item"
>
<span
class="ant-checkbox ant-checkbox-checked"
@ -7257,7 +7257,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<div
class="ant-input-number"
class="ant-input-number ant-input-number-in-form-item"
>
<div
class="ant-input-number-handler-wrap"

View File

@ -1207,20 +1207,6 @@ describe('Form', () => {
expect(subFormInstance).toBe(formInstance);
});
it('noStyle should not be affected by parent', () => {
const Demo = () => (
<Form>
<Form.Item>
<Form.Item noStyle>
<Select className="custom-select" />
</Form.Item>
</Form.Item>
</Form>
);
const { container } = render(<Demo />);
expect(container.querySelector('.custom-select')?.className).not.toContain('in-form-item');
});
it('noStyle should not affect status', () => {
const Demo = () => (
<Form>
@ -1237,12 +1223,22 @@ describe('Form', () => {
<Select className="custom-select-c" />
</Form.Item>
</Form.Item>
<Form.Item noStyle>
<Form.Item validateStatus="warning">
<Select className="custom-select-d" />
</Form.Item>
</Form.Item>
</Form>
);
const { container } = render(<Demo />);
expect(container.querySelector('.custom-select')?.className).toContain('status-error');
expect(container.querySelector('.custom-select')?.className).not.toContain('status-error');
expect(container.querySelector('.custom-select')?.className).not.toContain('in-form-item');
expect(container.querySelector('.custom-select-b')?.className).toContain('status-error');
expect(container.querySelector('.custom-select-c')?.className).toContain('status-warning');
expect(container.querySelector('.custom-select-b')?.className).toContain('in-form-item');
expect(container.querySelector('.custom-select-c')?.className).toContain('status-error');
expect(container.querySelector('.custom-select-c')?.className).toContain('in-form-item');
expect(container.querySelector('.custom-select-d')?.className).toContain('status-warning');
expect(container.querySelector('.custom-select-d')?.className).toContain('in-form-item');
});
it('should not affect Popup children style', () => {

View File

@ -1,8 +1,9 @@
import React from 'react';
import { render, fireEvent } from '../../../tests/utils';
import Image from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
const src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png';
@ -76,4 +77,17 @@ describe('Image', () => {
expect(baseElement.querySelector('.ant-image-preview')).toHaveClass('abc');
expect(baseElement.querySelector('.ant-image-preview-mask')).toHaveClass('def');
});
it('ConfigProvider getPopupContainer', () => {
const { container: wrapper, baseElement } = render(
<>
<div className="container" />
<ConfigProvider getPopupContainer={() => document.querySelector('.container')}>
<Image src={src} />
</ConfigProvider>
</>,
);
fireEvent.click(wrapper.querySelector('.ant-image'));
const containerElement = baseElement.querySelector('.container');
expect(containerElement.children.length).not.toBe(0);
});
});

View File

@ -20,7 +20,12 @@ const Image: CompositionImage<ImageProps> = ({
rootClassName,
...otherProps
}) => {
const { getPrefixCls, locale: contextLocale = defaultLocale } = useContext(ConfigContext);
const {
getPrefixCls,
locale: contextLocale = defaultLocale,
getPopupContainer: getContextPopupContainer,
} = useContext(ConfigContext);
const prefixCls = getPrefixCls('image', customizePrefixCls);
const rootPrefixCls = getPrefixCls();
@ -34,7 +39,7 @@ const Image: CompositionImage<ImageProps> = ({
return preview;
}
const _preview = typeof preview === 'object' ? preview : {};
const { getContainer, ...restPreviewProps } = _preview;
return {
mask: (
<div className={`${prefixCls}-mask-info`}>
@ -43,7 +48,8 @@ const Image: CompositionImage<ImageProps> = ({
</div>
),
icons,
..._preview,
...restPreviewProps,
getContainer: getContainer || getContextPopupContainer,
transitionName: getTransitionName(rootPrefixCls, 'zoom', _preview.transitionName),
maskTransitionName: getTransitionName(rootPrefixCls, 'fade', _preview.maskTransitionName),
};

View File

@ -6,12 +6,12 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext } from '../form/context';
import type { InputStatus } from '../_util/statusUtils';
import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import ClearableLabeledInput from './ClearableLabeledInput';
import type { InputFocusOptions } from './Input';
import { fixControlledValue, resolveOnChange, triggerFocus } from './Input';
@ -165,9 +165,8 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
// ============================== Reset ===============================
const handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
handleSetValue('', () => {
innerRef.current?.focus();
});
handleSetValue('');
innerRef.current?.focus();
resolveOnChange(innerRef.current?.resizableTextArea?.textArea!, e, onChange);
};

View File

@ -1,7 +1,8 @@
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import React, { useState } from 'react';
import Input from '..';
import focusTest from '../../../tests/shared/focusTest';
import { sleep, render, fireEvent, triggerResize } from '../../../tests/utils';
import { fireEvent, render, sleep, triggerResize } from '../../../tests/utils';
const { TextArea } = Input;
@ -530,4 +531,28 @@ describe('TextArea allowClear', () => {
rerender(<TextArea value={false} />);
expect(container.querySelector('textarea').value).toBe('false');
});
it('should focus when clearBtn is clicked in controlled case', () => {
const handleFocus = jest.fn();
const textareaSpy = spyElementPrototypes(HTMLTextAreaElement, {
focus: handleFocus,
});
const Demo = () => {
const [value, setValue] = React.useState('');
return <Input.TextArea allowClear value={value} onChange={e => setValue(e.target.value)} />;
};
const { container } = render(<Demo />);
fireEvent.change(container.querySelector('textarea'), { target: { value: 'test' } });
expect(container.querySelector('.ant-input-clear-icon')?.className).not.toContain(
'ant-input-clear-icon-hidden',
);
fireEvent.click(container.querySelector('.ant-input-clear-icon'));
expect(handleFocus).toHaveBeenCalledTimes(1);
textareaSpy.mockRestore();
});
});

View File

@ -127,7 +127,6 @@ The legacy demo code for version `<4.20.0` could be found at [https://github.com
| label | Menu label | ReactNode | - | |
| popupClassName | Sub-menu class name, not working when `mode="inline"` | string | - | |
| popupOffset | Sub-menu offset, not working when `mode="inline"` | \[number, number] | - | |
| title | Title of sub menu | ReactNode | - | |
| theme | Color theme of the SubMenu (inherits from Menu by default) | | `light` \| `dark` | - | |
| onTitleClick | Callback executed when the sub-menu title is clicked | function({ key, domEvent }) | - | |

View File

@ -1,13 +1,12 @@
import React from 'react';
import MockDate from 'mockdate';
import dayjs from 'dayjs';
import { mount } from 'enzyme';
import { fireEvent, render } from '@testing-library/react';
import MockDate from 'mockdate';
import React from 'react';
import Statistic from '..';
import { formatTimeStr } from '../utils';
import { sleep } from '../../../tests/utils';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import type Countdown from '../Countdown';
import { formatTimeStr } from '../utils';
describe('Statistic', () => {
mountTest(Statistic);
@ -23,32 +22,34 @@ describe('Statistic', () => {
});
it('`-` is not a number', () => {
const wrapper = mount(<Statistic value="-" />);
expect(wrapper.find('.ant-statistic-content').text()).toEqual('-');
const { container } = render(<Statistic value="-" />);
expect(container.querySelector('.ant-statistic-content')!.textContent).toEqual('-');
});
it('customize formatter', () => {
const formatter = jest.fn(() => 93);
const wrapper = mount(<Statistic value={1128} formatter={formatter} />);
const { container } = render(<Statistic value={1128} formatter={formatter} />);
expect(formatter).toHaveBeenCalledWith(1128);
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('93');
expect(container.querySelector('.ant-statistic-content-value')!.textContent).toEqual('93');
});
it('groupSeparator', () => {
const wrapper = mount(<Statistic value={1128} groupSeparator="__TEST__" />);
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('1__TEST__128');
const { container } = render(<Statistic value={1128} groupSeparator="__TEST__" />);
expect(container.querySelector('.ant-statistic-content-value')!.textContent).toEqual(
'1__TEST__128',
);
});
it('not a number', () => {
const wrapper = mount(<Statistic value="bamboo" />);
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual('bamboo');
const { container } = render(<Statistic value="bamboo" />);
expect(container.querySelector('.ant-statistic-content-value')!.textContent).toEqual('bamboo');
});
it('support negetive number', () => {
const wrapper = mount(
const { asFragment } = render(
<Statistic title="Account Balance (CNY)" value={-112893.12345} precision={2} />,
);
expect(wrapper.render()).toMatchSnapshot();
expect(asFragment().firstChild).toMatchSnapshot();
});
it('allow negetive precision', () => {
@ -58,54 +59,57 @@ describe('Statistic', () => {
[-3, -1112893.1212, '-1,112,893'],
[-1, -1112893, '-1,112,893'],
[-1, 1112893, '1,112,893'],
].forEach(([precision, value, expectValue]) => {
const wrapper = mount(<Statistic precision={precision} value={value} />);
expect(wrapper.find('.ant-statistic-content-value-int').text()).toEqual(expectValue);
expect(wrapper.find('.ant-statistic-content-value-decimal').length).toBe(0);
].forEach(([precision, value, expectValue]: [number, number, string]) => {
const { container } = render(<Statistic precision={precision} value={value} />);
expect(container.querySelector('.ant-statistic-content-value-int')!.textContent).toEqual(
expectValue,
);
expect(container.querySelectorAll('.ant-statistic-content-value-decimal').length).toBe(0);
});
});
it('loading with skeleton', async () => {
let loading = false;
const wrapper = mount(<Statistic title="Active Users" value={112112} loading={loading} />);
expect(wrapper.find('.ant-skeleton')).toHaveLength(0);
expect(wrapper.find('.ant-statistic-content')).toHaveLength(1);
const { container, rerender } = render(
<Statistic title="Active Users" value={112112} loading={loading} />,
);
expect(container.querySelectorAll('.ant-skeleton')).toHaveLength(0);
expect(container.querySelectorAll('.ant-statistic-content')).toHaveLength(1);
loading = true;
wrapper.setProps({ loading });
expect(wrapper.find('.ant-skeleton')).toHaveLength(1);
expect(wrapper.find('.ant-statistic-content')).toHaveLength(0);
rerender(<Statistic title="Active Users" value={112112} loading={loading} />);
expect(container.querySelectorAll('.ant-skeleton')).toHaveLength(1);
expect(container.querySelectorAll('.ant-statistic-content')).toHaveLength(0);
});
describe('Countdown', () => {
it('render correctly', () => {
const now = dayjs().add(2, 'd').add(11, 'h').add(28, 'm').add(9, 's').add(3, 'ms');
[
['H:m:s', '59:28:9'],
['HH:mm:ss', '59:28:09'],
['HH:mm:ss:SSS', '59:28:09:003'],
['DD-HH:mm:ss', '02-11:28:09'],
].forEach(([format, value]) => {
const wrapper = mount(<Statistic.Countdown format={format} value={now} />);
expect(wrapper.find('.ant-statistic-content-value').text()).toEqual(value);
const { container } = render(<Statistic.Countdown format={format} value={now} />);
expect(container.querySelector('.ant-statistic-content-value')!.textContent).toEqual(value);
});
});
it('time going', async () => {
const now = Date.now() + 1000;
const onFinish = jest.fn();
const wrapper = mount(<Statistic.Countdown value={now} onFinish={onFinish} />);
wrapper.update();
const instance = React.createRef<Countdown>();
const { unmount } = render(
<Statistic.Countdown ref={instance} value={now} onFinish={onFinish} />,
);
// setInterval should work
const instance = wrapper.find('Countdown').instance();
expect(instance.countdownId).not.toBe(undefined);
expect(instance.current!.countdownId).not.toBe(undefined);
await sleep(10);
wrapper.unmount();
expect(instance.countdownId).toBe(undefined);
unmount();
expect(onFinish).not.toHaveBeenCalled();
});
@ -115,21 +119,21 @@ describe('Statistic', () => {
const { container } = render(
<Statistic onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />,
);
fireEvent.mouseEnter(container.firstChild);
fireEvent.mouseEnter(container.firstChild!);
expect(onMouseEnter).toHaveBeenCalled();
fireEvent.mouseLeave(container.firstChild);
fireEvent.mouseLeave(container.firstChild!);
expect(onMouseLeave).toHaveBeenCalled();
});
it('responses hover events for Countdown', () => {
const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn();
const wrapper = mount(
const { container } = render(
<Statistic.Countdown onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />,
);
wrapper.simulate('mouseenter');
fireEvent.mouseEnter(container.firstChild!);
expect(onMouseEnter).toHaveBeenCalled();
wrapper.simulate('mouseleave');
fireEvent.mouseLeave(container.firstChild!);
expect(onMouseLeave).toHaveBeenCalled();
});
@ -138,11 +142,11 @@ describe('Statistic', () => {
const deadline = Date.now() + 10 * 1000;
let remainingTime;
const onChange = value => {
const onChange = (value: number) => {
remainingTime = value;
};
const wrapper = mount(<Statistic.Countdown value={deadline} onChange={onChange} />);
wrapper.update();
render(<Statistic.Countdown value={deadline} onChange={onChange} />);
// container.update();
await sleep(100);
expect(remainingTime).toBeGreaterThan(0);
});
@ -151,20 +155,18 @@ describe('Statistic', () => {
describe('time finished', () => {
it('not call if time already passed', () => {
const now = Date.now() - 1000;
const instance = React.createRef<Countdown>();
const onFinish = jest.fn();
const wrapper = mount(<Statistic.Countdown value={now} onFinish={onFinish} />);
wrapper.update();
render(<Statistic.Countdown ref={instance} value={now} onFinish={onFinish} />);
expect(wrapper.find('Countdown').instance().countdownId).toBe(undefined);
expect(instance.current!.countdownId).toBe(undefined);
expect(onFinish).not.toHaveBeenCalled();
});
it('called if finished', async () => {
const now = Date.now() + 10;
const onFinish = jest.fn();
const wrapper = mount(<Statistic.Countdown value={now} onFinish={onFinish} />);
wrapper.update();
render(<Statistic.Countdown value={now} onFinish={onFinish} />);
MockDate.set(dayjs('2019-11-28 00:00:00').valueOf());
await sleep(100);
expect(onFinish).toHaveBeenCalled();

File diff suppressed because it is too large Load Diff

View File

@ -976,7 +976,60 @@ exports[`Table.rowSelection fix selection column on the left when any other colu
</div>
`;
exports[`Table.rowSelection render with default selection correctly 1`] = `<div />`;
exports[`Table.rowSelection render with default selection correctly 1`] = `
<div
class="ant-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up"
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"
data-menu-id="rc-menu-uuid-test-all"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Select all data
</span>
</li>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
data-menu-id="rc-menu-uuid-test-invert"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Invert current page
</span>
</li>
<li
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
data-menu-id="rc-menu-uuid-test-none"
role="menuitem"
tabindex="-1"
>
<span
class="ant-dropdown-menu-title-content"
>
Clear all data
</span>
</li>
</ul>
<div
aria-hidden="true"
style="display: none;"
/>
</div>
`;
exports[`Table.rowSelection should support getPopupContainer 1`] = `
<div

View File

@ -26,7 +26,7 @@ const App: React.FC = () => {
const handleUpload = () => {
const formData = new FormData();
fileList.forEach(file => {
formData.append('files[]', file.originFileObj as RcFile);
formData.append('files[]', file as RcFile);
});
setUploading(true);
// You can use any AJAX library you like

View File

@ -1,6 +1,6 @@
{
"name": "antd",
"version": "4.21.0",
"version": "4.21.2",
"description": "An enterprise-class UI design language and React components implementation",
"title": "Ant Design",
"keywords": [
@ -287,10 +287,10 @@
"scrollama": "^3.0.0",
"semver": "^7.3.5",
"simple-git": "^3.0.0",
"stylelint": "14.8.3",
"stylelint": "^14.9.0",
"stylelint-config-prettier": "^9.0.2",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^25.0.0",
"stylelint-config-standard": "^26.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.1.0",
"stylelint-order": "^5.0.0",
"theme-switcher": "^1.0.2",

View File

@ -10,7 +10,9 @@
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"jsx": "preserve",
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"noUnusedParameters": true,
"noUnusedLocals": true,
"noImplicitAny": true,