Merge branch 'master' into next-merge-master

This commit is contained in:
MadCcc 2022-08-15 15:20:39 +08:00
commit e6335e233c
43 changed files with 5078 additions and 667 deletions

View File

@ -1,4 +1,4 @@
{
"sandboxes": ["antd-reproduction-template-6e93z"],
"sandboxes": ["antd-reproduction-template-y9vgcf"],
"node": "14"
}

View File

@ -40,7 +40,7 @@ jobs:
triger: 'tag'
changelogs: 'CHANGELOG.en-US.md, CHANGELOG.zh-CN.md'
branch: 'master'
dingding-token: ${{ secrets.DINGDING_BOT_BIGFISH_TOKEN }}
dingding-token: ${{ secrets.DINGDING_BOT_BIGFISH_TOKEN }} ${{ secrets.DINGDING_BOT_YUNFENGDIE_TOKEN }}
dingding-msg: 'CHANGELOG.zh-CN.md'
dingding-delay-minute: 10
release: false

View File

@ -15,6 +15,31 @@ timeline: true
---
## 4.22.5
`2022-08-15`
- 🇭🇺 Form add defaultValidateMessages.i18n az_AZ language #23369. [#36967](https://github.com/ant-design/ant-design/pull/36967) [@YMiemie-cy](https://github.com/YMiemie-cy)
- 💄 Popover remove Popover incorrect box-shadow. [#37030](https://github.com/ant-design/ant-design/pull/37030) [@jerrykingxyz](https://github.com/jerrykingxyz)
- 🐞 Fix Steps tail position bug when `labelPlacement="vertical"`. [#36996](https://github.com/ant-design/ant-design/pull/36996)
- 💄 Removed exception error border for `Pagination`. [#36972](https://github.com/ant-design/ant-design/pull/36972) [@hydraZty](https://github.com/hydraZty)
- 🐞 Fix Upload trigger mistake files status when upload multiple files at same time in React 18. [#36968](https://github.com/ant-design/ant-design/pull/36968)
- TypeScript
- 🤖 Fix type error of useWatch. [#37013](https://github.com/ant-design/ant-design/pull/37013) [@LiZhiHao97](https://github.com/LiZhiHao97)
## 4.22.4
`2022-08-08`
- 🐞 Fix Drawer `zIndex` prop not working. [#36958](https://github.com/ant-design/ant-design/pull/36958)
- 💄 Fix PageHeader focus style stay after clicking it. [#36902](https://github.com/ant-design/ant-design/pull/36902)
- 💄 Fix Drawer nesting content-wrapper style. [#36845](https://github.com/ant-design/ant-design/pull/36845) [@yanm1ng](https://github.com/yanm1ng)
- 🐞 Fix Mentions popup shift when contain scrollbar. [#36898](https://github.com/ant-design/ant-design/pull/36898) [@JarvisArt](https://github.com/JarvisArt)
- TypeScript
- 🤖 Fix Avatar `onClick` type missing. [#36940](https://github.com/ant-design/ant-design/pull/36940) [@kungege](https://github.com/kungege)
- 🤖 Fix Table `onChange` argument `sorter` type error. [#36710](https://github.com/ant-design/ant-design/pull/36710) [@kungege](https://github.com/kungege)
- 🤖 Input `data-*` should not be required. [#36858](https://github.com/ant-design/ant-design/pull/36858) [@yifanwww](https://github.com/yifanwww)
## 4.22.3
`2022-08-01`

View File

@ -15,6 +15,31 @@ timeline: true
---
## 4.22.5
`2022-08-15`
- 🇭🇺 Form `defaultValidateMessages.i18n` 增加匈牙利语。[#36967](https://github.com/ant-design/ant-design/pull/36967) [@YMiemie-cy](https://github.com/YMiemie-cy)
- 💄 移除 Popover 中无效的 `box-shadow` 样式。[#37030](https://github.com/ant-design/ant-design/pull/37030) [@jerrykingxyz](https://github.com/jerrykingxyz)
- 🐞 修复 Steps `labelPlacement="vertical"` 时连接线位置偏上的问题。[#36996](https://github.com/ant-design/ant-design/pull/36996)
- 💄 去掉 Pagination 的异常错误边框。[#36972](https://github.com/ant-design/ant-design/pull/36972) [@hydraZty](https://github.com/hydraZty)
- 🐞 修复 Upload 在 React 18 下同时上传多份文件会出现上传状态不正确的问题。[#36968](https://github.com/ant-design/ant-design/pull/36968)
- TypeScript
- 🤖 修复 useWatch 的类型错误。[#37013](https://github.com/ant-design/ant-design/pull/37013) [@LiZhiHao97](https://github.com/LiZhiHao97)
## 4.22.4
`2022-08-08`
- 🐞 修复 Drawer `zIndex` 属性失效的问题。[#36958](https://github.com/ant-design/ant-design/pull/36958)
- 💄 修复 PageHeader 返回按钮点击后依然有聚焦样式的问题。[#36902](https://github.com/ant-design/ant-design/pull/36902)
- 💄 修复抽屉 Drawer 组件嵌套时,子抽屉的样式被父抽屉影响的问题。[#36845](https://github.com/ant-design/ant-design/pull/36845) [@yanm1ng](https://github.com/yanm1ng)
- 🐞 修复 Mentions 内有滚动条时弹出层偏移的问题。[#36898](https://github.com/ant-design/ant-design/pull/36898) [@JarvisArt](https://github.com/JarvisArt)
- TypeScript
- 🤖 修复 Avatar `onClick` 属性定义丢失的问题。[#36940](https://github.com/ant-design/ant-design/pull/36940) [@kungege](https://github.com/kungege)
- 🤖 修复 Table `onChange` 参数 `sorter` 类型报错的问题。[#36710](https://github.com/ant-design/ant-design/pull/36710) [@kungege](https://github.com/kungege)
- 🤖 Input `data-*` 属性将不再是必传参数。[#36858](https://github.com/ant-design/ant-design/pull/36858) [@yifanwww](https://github.com/yifanwww)
## 4.22.3
`2022-08-01`

View File

@ -1,121 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AutoComplete legacy dataSource should accept react element option 1`] = `
<div
class="ant-select ant-select-auto-complete ant-select-single ant-select-open ant-select-show-search"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_-1"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-expanded="true"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
role="combobox"
type="search"
value=""
/>
</span>
<span
class="ant-select-selection-placeholder"
/>
</div>
<div>
<div
class="ant-select-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up"
style="opacity: 0; min-width: 0; width: 0px;"
>
<div>
<div
id="rc_select_TEST_OR_SSR_list"
role="listbox"
style="height: 0px; width: 0px; overflow: hidden;"
>
<div
aria-label="ReactNode"
aria-selected="false"
id="rc_select_TEST_OR_SSR_list_0"
role="option"
>
key
</div>
</div>
<div
class="rc-virtual-list"
style="position: relative;"
>
<div
class="rc-virtual-list-holder"
style="max-height: 256px; overflow-y: auto;"
>
<div>
<div
class="rc-virtual-list-holder-inner"
style="display: flex; flex-direction: column;"
>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="ReactNode"
>
<div
class="ant-select-item-option-content"
>
ReactNode
</div>
<span
aria-hidden="true"
class="ant-select-item-option-state"
style="user-select: none;"
unselectable="on"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`AutoComplete rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-select ant-select-rtl ant-select-auto-complete ant-select-single ant-select-show-search"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
role="combobox"
type="search"
value=""
/>
</span>
<span
class="ant-select-selection-placeholder"
/>
</div>
</div>
`;

View File

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AutoComplete rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-select ant-select-rtl ant-select-auto-complete ant-select-single ant-select-show-search"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
role="combobox"
type="search"
value=""
/>
</span>
<span
class="ant-select-selection-placeholder"
/>
</div>
</div>
`;

View File

@ -1,30 +1,35 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import AutoComplete from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import { render, screen } from '../../../tests/utils';
import Input from '../../input';
describe('AutoComplete', () => {
mountTest(AutoComplete);
rtlTest(AutoComplete);
it('AutoComplete with custom Input render perfectly', () => {
const { container } = render(
it('AutoComplete with custom Input render perfectly', async () => {
render(
<AutoComplete dataSource={['12345', '23456', '34567']}>
<textarea />
</AutoComplete>,
);
expect(container.querySelectorAll('textarea').length).toBe(1);
fireEvent.change(container.querySelector('textarea'), { target: { value: '123' } });
expect(screen.getByRole('combobox')).toBeInTheDocument();
// should not filter data source defaultly
expect(container.querySelectorAll('.ant-select-item-option').length).toBe(3);
// should show options when type in input
await userEvent.type(screen.getByRole('combobox'), '123');
// should not filter data source by default
expect(screen.getByTitle('12345')).toBeInTheDocument();
expect(screen.getByTitle('23456')).toBeInTheDocument();
expect(screen.getByTitle('34567')).toBeInTheDocument();
});
it('AutoComplete should work when dataSource is object array', () => {
const { container } = render(
it('AutoComplete should work when dataSource is object array', async () => {
render(
<AutoComplete
dataSource={[
{ text: 'text', value: 'value' },
@ -34,17 +39,19 @@ describe('AutoComplete', () => {
<input />
</AutoComplete>,
);
expect(container.querySelectorAll('input').length).toBe(1);
fireEvent.change(container.querySelector('input'), { target: { value: 'a' } });
expect(screen.getByRole('combobox')).toBeInTheDocument();
await userEvent.type(screen.getByRole('combobox'), 'a');
// should not filter data source defaultly
expect(container.querySelectorAll('.ant-select-item-option').length).toBe(2);
// should not filter data source by default
expect(screen.getByTitle('text')).toBeInTheDocument();
expect(screen.getByTitle('abc')).toBeInTheDocument();
});
it('AutoComplete throws error when contains invalid dataSource', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
render(
// @ts-ignore
<AutoComplete dataSource={[() => {}]}>
<textarea />
</AutoComplete>,
@ -54,22 +61,23 @@ describe('AutoComplete', () => {
});
it('legacy dataSource should accept react element option', () => {
const { asFragment } = render(
<AutoComplete open dataSource={[<span key="key">ReactNode</span>]} />,
);
expect(asFragment().firstChild).toMatchSnapshot();
render(<AutoComplete open dataSource={[<span key="key">ReactNode</span>]} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByTitle(/reactnode/i)).toBeInTheDocument();
});
it('legacy AutoComplete.Option should be compatiable', () => {
const { container } = render(
it('legacy AutoComplete.Option should be compatiable', async () => {
render(
<AutoComplete>
<AutoComplete.Option value="111">111</AutoComplete.Option>
<AutoComplete.Option value="222">222</AutoComplete.Option>
</AutoComplete>,
);
expect(container.querySelectorAll('input').length).toBe(1);
fireEvent.change(container.querySelector('input'), { target: { value: '1' } });
expect(container.querySelectorAll('.ant-select-item-option').length).toBe(2);
expect(screen.getByRole('combobox')).toBeInTheDocument();
await userEvent.type(screen.getByRole('combobox'), '1');
expect(screen.getByTitle(/111/i)).toBeInTheDocument();
expect(screen.getByTitle(/222/i)).toBeInTheDocument();
});
it('should not warning when getInputElement is null', () => {
@ -77,17 +85,18 @@ describe('AutoComplete', () => {
render(<AutoComplete placeholder="input here" allowClear />);
// eslint-disable-next-line no-console
expect(console.warn).not.toBeCalled();
// @ts-ignore
// eslint-disable-next-line no-console
console.warn.mockRestore();
});
it('should not override custom input className', () => {
const { container } = render(
render(
<AutoComplete>
<Input className="custom" />
</AutoComplete>,
);
expect(container.querySelector('input').classList.contains('custom')).toBeTruthy();
expect(screen.getByRole('combobox')).toHaveClass('custom');
});
it('should show warning when use dropdownClassName', () => {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,453 @@
import { mount } from 'enzyme';
import MockDate from 'mockdate';
import Moment from 'moment';
import momentGenerateConfig from 'rc-picker/lib/generate/moment';
import type { Locale } from 'rc-picker/lib/interface';
import React from 'react';
import Calendar from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import Group from '../../radio/group';
import Button from '../../radio/radioButton';
import Select from '../../select';
import Header from '../Header';
describe('Calendar', () => {
mountTest(Calendar);
rtlTest(Calendar, true);
function openSelect(wrapper: HTMLElement, className: string) {
fireEvent.mouseDown(wrapper.querySelector(className)!.querySelector('.ant-select-selector')!);
}
function findSelectItem(wrapper: HTMLElement) {
return wrapper.querySelectorAll('.ant-select-item-option')!;
}
function clickSelectItem(wrapper: HTMLElement, index = 0) {
fireEvent.click(findSelectItem(wrapper)[index]);
}
// https://github.com/ant-design/ant-design/issues/30392
it('should be able to set undefined or null', () => {
expect(() => {
const wrapper = mount(<Calendar />);
wrapper.setProps({ value: null });
}).not.toThrow();
expect(() => {
const wrapper = mount(<Calendar />);
wrapper.setProps({ value: undefined });
}).not.toThrow();
});
it('Calendar should be selectable', () => {
MockDate.set(Moment('2000-01-01').valueOf());
const onSelect = jest.fn();
const onChange = jest.fn();
const { container } = render(<Calendar onSelect={onSelect} onChange={onChange} />);
fireEvent.click(container.querySelector('.ant-picker-cell')!);
expect(onSelect).toHaveBeenCalledWith(expect.anything());
const value = onSelect.mock.calls[0][0];
expect(Moment.isMoment(value)).toBe(true);
expect(onChange).toHaveBeenCalled();
MockDate.reset();
});
it('only Valid range should be selectable', () => {
const onSelect = jest.fn();
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2018-02-18')];
const wrapper = mount(
<Calendar onSelect={onSelect} validRange={validRange} defaultValue={Moment('2018-02-02')} />,
);
wrapper.find('[title="2018-02-01"]').at(0).simulate('click');
wrapper.find('[title="2018-02-02"]').at(0).simulate('click');
expect(onSelect.mock.calls.length).toBe(1);
});
it('dates other than in valid range should be disabled', () => {
const onSelect = jest.fn();
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2018-02-18')];
const { container } = render(
<Calendar onSelect={onSelect} validRange={validRange} defaultValue={Moment('2018-02-02')} />,
);
fireEvent.click(container.querySelector('[title="2018-02-20"]')!);
const elem = container
.querySelector('[title="2018-02-20"]')!
.className.includes('ant-picker-cell-disabled');
expect(elem).toEqual(true);
expect(onSelect.mock.calls.length).toBe(0);
});
it('months other than in valid range should be disabled', () => {
const onSelect = jest.fn();
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2018-05-18')];
const { container } = render(
<Calendar
onSelect={onSelect}
validRange={validRange}
defaultValue={Moment('2018-02-02')}
mode="year"
/>,
);
expect(
container.querySelector('[title="2018-01"]')?.className.includes('ant-picker-cell-disabled'),
).toBe(true);
expect(
container.querySelector('[title="2018-02"]')?.className.includes('ant-picker-cell-disabled'),
).toBe(false);
expect(
container.querySelector('[title="2018-06"]')?.className.includes('ant-picker-cell-disabled'),
).toBe(true);
fireEvent.click(container.querySelector('[title="2018-01"]')!);
fireEvent.click(container.querySelector('[title="2018-03"]')!);
expect(onSelect.mock.calls.length).toBe(1);
});
it('months other than in valid range should not be shown in header', () => {
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2017-02-02'), Moment('2018-05-18')];
const { container } = render(<Calendar validRange={validRange} />);
openSelect(container, '.ant-picker-calendar-year-select');
clickSelectItem(container);
openSelect(container, '.ant-picker-calendar-month-select');
// 2 years and 11 months
expect(container.querySelectorAll('.ant-select-item-option').length).toBe(13);
});
it('getDateRange should returns a disabledDate function', () => {
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2018-05-18')];
const wrapper = mount(<Calendar validRange={validRange} defaultValue={Moment('2018-02-02')} />);
const { disabledDate } = wrapper.find('PickerPanel').props() as any;
expect(disabledDate(Moment('2018-06-02'))).toBe(true);
expect(disabledDate(Moment('2018-04-02'))).toBe(false);
});
it('validRange should work with disabledDate function', () => {
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2018-05-18')];
const wrapper = mount(
<Calendar validRange={validRange} disabledDate={data => data.isSame(Moment('2018-02-03'))} />,
);
const { disabledDate } = wrapper.find('PickerPanel').props() as any;
expect(disabledDate(Moment('2018-02-01'))).toBe(true);
expect(disabledDate(Moment('2018-02-02'))).toBe(false);
expect(disabledDate(Moment('2018-02-03'))).toBe(true);
expect(disabledDate(Moment('2018-02-04'))).toBe(false);
expect(disabledDate(Moment('2018-06-01'))).toBe(true);
});
it('Calendar MonthSelect should display correct label', () => {
const validRange: [Moment.Moment, Moment.Moment] = [Moment('2018-02-02'), Moment('2019-06-1')];
const wrapper = mount(<Calendar validRange={validRange} defaultValue={Moment('2019-01-01')} />);
expect(wrapper.render()).toMatchSnapshot();
});
it('Calendar should change mode by prop', () => {
const monthMode = 'month';
const yearMode = 'year';
const wrapper = mount(<Calendar />);
expect(wrapper.find('CalendarHeader').props().mode).toEqual(monthMode);
wrapper.setProps({ mode: yearMode });
expect(wrapper.find('CalendarHeader').props().mode).toEqual(yearMode);
});
it('Calendar should switch mode', () => {
const monthMode = 'month';
const yearMode = 'year';
const onPanelChangeStub = jest.fn();
const wrapper = mount(<Calendar mode={yearMode} onPanelChange={onPanelChangeStub} />);
expect(wrapper.find('CalendarHeader').props().mode).toEqual(yearMode);
wrapper.setProps({ mode: monthMode });
expect(wrapper.find('CalendarHeader').props().mode).toEqual(monthMode);
expect(onPanelChangeStub).toHaveBeenCalledTimes(0);
});
it('Calendar should support locale', () => {
MockDate.set(Moment('2018-10-19').valueOf());
// eslint-disable-next-line global-require
const zhCN = require('../locale/zh_CN').default;
const wrapper = mount(<Calendar locale={zhCN} />);
expect(wrapper.render()).toMatchSnapshot();
MockDate.reset();
});
describe('onPanelChange', () => {
it('trigger when click last month of date', () => {
const onPanelChange = jest.fn();
const date = Moment('1990-09-03');
const wrapper = mount(<Calendar onPanelChange={onPanelChange} value={date} />);
wrapper.find('.ant-picker-cell').at(0).simulate('click');
expect(onPanelChange).toHaveBeenCalled();
expect(onPanelChange.mock.calls[0][0].month()).toEqual(date.month() - 1);
});
it('not trigger when in same month', () => {
const onPanelChange = jest.fn();
const date = Moment('1990-09-03');
const wrapper = mount(<Calendar onPanelChange={onPanelChange} value={date} />);
wrapper.find('.ant-picker-cell').at(10).simulate('click');
expect(onPanelChange).not.toHaveBeenCalled();
});
});
it('switch should work correctly without prop mode', async () => {
const onPanelChange = jest.fn();
const date = Moment(new Date(Date.UTC(2017, 7, 9, 8)));
const wrapper = mount(<Calendar onPanelChange={onPanelChange} value={date} />);
expect(wrapper.find('CalendarHeader').props().mode).toBe('month');
expect(wrapper.find('.ant-picker-date-panel').length).toBe(1);
expect(wrapper.find('.ant-picker-month-panel').length).toBe(0);
wrapper.find('.ant-radio-button-input[value="year"]').simulate('change');
expect(wrapper.find('.ant-picker-date-panel').length).toBe(0);
expect(wrapper.find('.ant-picker-month-panel').length).toBe(1);
expect(onPanelChange).toHaveBeenCalled();
expect(onPanelChange.mock.calls[0][1]).toEqual('year');
});
const createWrapper = (
start: Moment.Moment,
end: Moment.Moment,
value: Moment.Moment,
onValueChange: (v: Moment.Moment) => void,
) => {
const wrapper = render(
// @ts-ignore
<Header
prefixCls="ant-picker-calendar"
generateConfig={momentGenerateConfig}
onChange={onValueChange}
value={value}
validRange={[start, end]}
locale={{ year: '年' } as Locale}
/>,
);
openSelect(wrapper.container, '.ant-picker-calendar-year-select');
clickSelectItem(wrapper.container);
};
it('if value.month > end.month, set value.month to end.month', () => {
const value = Moment('1990-01-03');
const start = Moment('2019-04-01');
const end = Moment('2019-11-01');
const onValueChange = jest.fn();
createWrapper(start, end, value, onValueChange);
expect(onValueChange).toHaveBeenCalledWith(value.year(2019).month('3'));
});
it('if start.month > value.month, set value.month to start.month', () => {
const value = Moment('1990-01-03');
const start = Moment('2019-11-01');
const end = Moment('2019-03-01');
const onValueChange = jest.fn();
createWrapper(start, end, value, onValueChange);
expect(onValueChange).toHaveBeenCalledWith(value.year(2019).month('10'));
});
it('if change year and month > end month, set value.month to end.month', () => {
const value = Moment('2018-11-03');
const start = Moment('2000-01-01');
const end = Moment('2019-03-01');
const onValueChange = jest.fn();
const wrapper = render(
<Header
prefixCls="ant-picker-calendar"
generateConfig={momentGenerateConfig}
onChange={onValueChange}
value={value}
validRange={[start, end]}
// @ts-ignore
locale={{ year: '年' }}
/>,
);
openSelect(wrapper.container, '.ant-picker-calendar-year-select');
fireEvent.click(
Array.from(wrapper.container.querySelectorAll('.ant-select-item-option')).at(-1)!,
);
expect(onValueChange).toHaveBeenCalledWith(value.year(2019).month('2'));
});
it('onMonthChange should work correctly', () => {
const start = Moment('2018-11-01');
const end = Moment('2019-03-01');
const value = Moment('2018-12-03');
const onValueChange = jest.fn();
const wrapper = render(
<Header
prefixCls="ant-picker-calendar"
generateConfig={momentGenerateConfig}
onChange={onValueChange}
value={value}
validRange={[start, end]}
// @ts-ignore
locale={{ year: '年', locale: 'zh_CN' }}
mode="month"
/>,
);
openSelect(wrapper.container, '.ant-picker-calendar-month-select');
clickSelectItem(wrapper.container);
expect(onValueChange).toHaveBeenCalledWith(value.month(10));
});
it('onTypeChange should work correctly', () => {
const onTypeChange = jest.fn();
const value = Moment('2018-12-03');
const wrapper = render(
<Header
prefixCls="ant-picker-calendar"
generateConfig={momentGenerateConfig}
onModeChange={onTypeChange}
locale={{ year: '年', month: '月', locale: 'zh_CN' } as any}
value={value}
// @ts-ignore
type="date"
/>,
);
fireEvent.click(
Array.from(wrapper.container.querySelectorAll(`.ant-radio-button-input`)).at(1)!,
);
expect(onTypeChange).toHaveBeenCalledWith('year');
});
it('headerRender should work correctly', () => {
const onMonthChange = jest.fn();
const onYearChange = jest.fn();
const onTypeChange = jest.fn();
// Year
const headerRender = jest.fn(({ value }) => {
const year = value.year();
const options = [];
for (let i = year - 100; i < year + 100; i += 1) {
options.push(
<Select.Option className="year-item" key={i} value={i}>
{i}
</Select.Option>,
);
}
return (
<Select
size="small"
dropdownMatchSelectWidth={false}
className="my-year-select"
onChange={onYearChange}
value={String(year)}
>
{options}
</Select>
);
});
const uiWithYear = <Calendar fullscreen={false} headerRender={headerRender} />;
const wrapperWithYear = render(uiWithYear);
openSelect(wrapperWithYear.container, '.ant-select');
wrapperWithYear.rerender(uiWithYear);
fireEvent.click(Array.from(findSelectItem(wrapperWithYear.container)).at(-1)!);
expect(onYearChange).toHaveBeenCalled();
// Month
const headerRenderWithMonth = jest.fn(({ value }) => {
const start = 0;
const end = 12;
const monthOptions = [];
const current = value.clone();
const localeData = value.localeData();
const months = [];
for (let i = 0; i < 12; i += 1) {
current.month(i);
months.push(localeData.monthsShort(current));
}
for (let index = start; index < end; index += 1) {
monthOptions.push(
<Select.Option className="month-item" key={`${index}`} value={index}>
{months[index]}
</Select.Option>,
);
}
const month = value.month();
return (
<Select
size="small"
dropdownMatchSelectWidth={false}
className="my-month-select"
onChange={onMonthChange}
value={String(month)}
>
{monthOptions}
</Select>
);
});
const uiWithMonth = <Calendar fullscreen={false} headerRender={headerRenderWithMonth} />;
const wrapperWithMonth = render(uiWithMonth);
openSelect(wrapperWithMonth.container, '.ant-select');
wrapperWithMonth.rerender(uiWithMonth);
fireEvent.click(Array.from(findSelectItem(wrapperWithMonth.container)).at(-1)!);
expect(onMonthChange).toHaveBeenCalled();
// Type
const headerRenderWithTypeChange = jest.fn(({ type }) => (
<Group size="small" onChange={onTypeChange} value={type}>
<Button value="month">Month</Button>
<Button value="year">Year</Button>
</Group>
));
const wrapperWithTypeChange = render(
<Calendar fullscreen={false} headerRender={headerRenderWithTypeChange} />,
);
fireEvent.click(
Array.from(wrapperWithTypeChange.container.querySelectorAll('.ant-radio-button-input')).at(
-1,
)!,
);
expect(onTypeChange).toHaveBeenCalled();
});
it('dateFullCellRender', () => {
const { container } = render(
<Calendar dateFullCellRender={() => <div className="light">Bamboo</div>} />,
);
expect(container.querySelectorAll('.light')[0].innerHTML).toEqual('Bamboo');
});
it('monthFullCellRender', () => {
const { container } = render(
<Calendar mode="year" monthFullCellRender={() => <div className="bamboo">Light</div>} />,
);
expect(container.querySelectorAll('.bamboo')[0].innerHTML).toEqual('Light');
});
it('when fullscreen is false, the element returned by dateFullCellRender should be interactive', () => {
const onClick = jest.fn();
const { container } = render(
<Calendar
fullscreen={false}
dateFullCellRender={() => (
<div className="bamboo" onClick={onClick}>
Light
</div>
)}
/>,
);
fireEvent.click(container.querySelectorAll('.bamboo')[0]);
expect(onClick).toBeCalled();
});
});

View File

@ -1,9 +1,9 @@
import { mount } from 'enzyme';
import MockDate from 'mockdate';
import React from 'react';
import Descriptions from '..';
import mountTest from '../../../tests/shared/mountTest';
import { resetWarned } from '../../_util/warning';
import { render } from '../../../tests/utils';
describe('Descriptions', () => {
mountTest(Descriptions);
@ -20,7 +20,7 @@ describe('Descriptions', () => {
});
it('when max-width: 575pxcolumn=1', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
@ -29,14 +29,14 @@ describe('Descriptions', () => {
<Descriptions.Item>No-Label</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('tr')).toHaveLength(5);
expect(wrapper.find('.ant-descriptions-item-label')).toHaveLength(4);
expect(wrapper.container.querySelectorAll('tr')).toHaveLength(5);
expect(wrapper.container.querySelectorAll('.ant-descriptions-item-label')).toHaveLength(4);
wrapper.unmount();
});
it('when max-width: 575pxcolumn=2', () => {
// eslint-disable-next-line global-require
const wrapper = mount(
const wrapper = render(
<Descriptions column={{ xs: 2 }}>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
@ -44,26 +44,26 @@ describe('Descriptions', () => {
<Descriptions.Item label="Amount">$80.00</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('tr')).toHaveLength(2);
expect(wrapper.container.querySelectorAll('tr')).toHaveLength(2);
wrapper.unmount();
});
it('column is number', () => {
// eslint-disable-next-line global-require
const wrapper = mount(
<Descriptions column="3">
const wrapper = render(
<Descriptions column={3}>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
<Descriptions.Item label="time">18:00:00</Descriptions.Item>
<Descriptions.Item label="Amount">$80.00</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
wrapper.unmount();
});
it('when typeof column is object', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions column={{ xs: 8, sm: 16, md: 24 }}>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
@ -71,14 +71,19 @@ describe('Descriptions', () => {
<Descriptions.Item label="Amount">$80.00</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('td').reduce((total, td) => total + td.props().colSpan, 0)).toBe(8);
expect(
Array.from(wrapper.container.querySelectorAll('td'))
.map(i => Number(i.getAttribute('colspan')))
.filter(Boolean)
.reduce((total, cur) => total + cur, 0),
).toBe(8);
wrapper.unmount();
});
it('warning if ecceed the row span', () => {
resetWarned();
mount(
render(
<Descriptions column={3}>
<Descriptions.Item label="Product" span={2}>
Cloud Database
@ -95,7 +100,7 @@ describe('Descriptions', () => {
it('when item is rendered conditionally', () => {
const hasDiscount = false;
const wrapper = mount(
const wrapper = render(
<Descriptions>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
@ -104,13 +109,13 @@ describe('Descriptions', () => {
{hasDiscount && <Descriptions.Item label="Discount">$20.00</Descriptions.Item>}
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
wrapper.unmount();
});
it('vertical layout', () => {
// eslint-disable-next-line global-require
const wrapper = mount(
const wrapper = render(
<Descriptions layout="vertical">
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing">Prepaid</Descriptions.Item>
@ -118,47 +123,46 @@ describe('Descriptions', () => {
<Descriptions.Item label="Amount">$80.00</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
wrapper.unmount();
});
it('Descriptions.Item support className', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions>
<Descriptions.Item label="Product" className="my-class">
Cloud Database
</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
});
it('Descriptions support colon', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions colon={false}>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
});
it('Descriptions support style', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions style={{ backgroundColor: '#e8e8e8' }}>
<Descriptions.Item>Cloud Database</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
});
it('keep key', () => {
const wrapper = mount(
render(
<Descriptions>
<Descriptions.Item key="bamboo" />
<Descriptions.Item key="bamboo">1</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('Cell').key()).toBe('item-bamboo');
expect(jest.spyOn(document, 'createElement')).not.toBeCalled();
});
// https://github.com/ant-design/ant-design/issues/19887
@ -166,7 +170,7 @@ describe('Descriptions', () => {
if (!React.Fragment) {
return;
}
const wrapper = mount(
const wrapper = render(
<Descriptions>
<Descriptions.Item label="bamboo">bamboo</Descriptions.Item>
<>
@ -176,12 +180,12 @@ describe('Descriptions', () => {
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/issues/20255
it('columns 5 with customize', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions layout="vertical" column={4}>
{/* 1 1 1 1 */}
<Descriptions.Item label="bamboo">bamboo</Descriptions.Item>
@ -203,12 +207,12 @@ describe('Descriptions', () => {
</Descriptions>,
);
function matchSpan(rowIndex, spans) {
const tr = wrapper.find('tr').at(rowIndex);
const tds = tr.find('th');
function matchSpan(rowIndex: number, spans: number[]) {
const trs = Array.from(wrapper.container.querySelectorAll('tr')).at(rowIndex);
const tds = Array.from(trs?.querySelectorAll('th')!);
expect(tds).toHaveLength(spans.length);
tds.forEach((td, index) => {
expect(td.props().colSpan).toEqual(spans[index]);
expect(Number(td.getAttribute('colSpan'))).toEqual(spans[index]);
});
}
@ -218,35 +222,38 @@ describe('Descriptions', () => {
});
it('number value should render correct', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions bordered>
<Descriptions.Item label={0}>{0}</Descriptions.Item>
<Descriptions.Item label={0}>0</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('th').hasClass('ant-descriptions-item-label')).toBeTruthy();
expect(wrapper.find('td').hasClass('ant-descriptions-item-content')).toBeTruthy();
expect(wrapper.container.querySelector('th')).toHaveClass('ant-descriptions-item-label');
expect(wrapper.container.querySelector('td')).toHaveClass('ant-descriptions-item-content');
});
it('Descriptions support extra', () => {
const wrapper = mount(
const wrapper1 = render(
<Descriptions extra="Edit">
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.find('.ant-descriptions-extra').exists()).toBe(true);
wrapper.setProps({ extra: undefined });
expect(wrapper.find('.ant-descriptions-extra').exists()).toBe(false);
const wrapper2 = render(
<Descriptions>
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
</Descriptions>,
);
expect(wrapper1.container.querySelector('.ant-descriptions-extra')).toBeTruthy();
expect(wrapper2.container.querySelector('.ant-descriptions-extra')).toBeFalsy();
});
it('number 0 should render correct', () => {
const wrapper = mount(
const wrapper = render(
<Descriptions>
<Descriptions.Item label={0} labelStyle={{ color: 'red' }} contentStyle={{ color: 'red' }}>
{0}
</Descriptions.Item>
</Descriptions>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(wrapper.container.firstChild).toMatchSnapshot();
});
});

View File

@ -198,4 +198,11 @@ describe('Drawer', () => {
errorSpy.mockRestore();
});
it('zIndex should work', () => {
const { container } = render(<Drawer getContainer={false} visible zIndex={903} />);
expect(container.querySelector('.ant-drawer')).toHaveStyle({
zIndex: 903,
});
});
});

View File

@ -55,7 +55,7 @@ function Drawer({
drawerStyle,
visible,
children,
zIndex,
style,
title,
headerStyle,
onClose,

View File

@ -315,7 +315,7 @@ export default () => {
### Form.useWatch
`type Form.useWatch = (namePath: NamePath, formInstance: FormInstance): Value`
`type Form.useWatch = (namePath: NamePath, formInstance?: FormInstance): Value`
Added in `4.20.0`. Watch the value of a field. You can use this to interactive with other hooks like `useSWR` to reduce develop cost:
@ -343,9 +343,9 @@ const Demo = () => {
Added in `4.22.0`. Could be used to get validate status of Form.Item. If this hook is not used under Form.Item, `status` would be `undefined`:
```tsx
const CustomInput = ({ value }) => {
const CustomInput = ({ value, onChange }) => {
const { status } = Form.Item.useStatus();
return <input value={value} className={`custom-input-${status}`} />;
return <input value={value} onChange={onChange} className={`custom-input-${status}`} />;
};
export default () => (

View File

@ -314,7 +314,7 @@ export default () => {
### Form.useWatch
`type Form.useWatch = (namePath: NamePath, formInstance: FormInstance): Value`
`type Form.useWatch = (namePath: NamePath, formInstance?: FormInstance): Value`
`4.20.0` 新增,用于直接获取 form 中字段对应的值。通过该 Hooks 可以与诸如 `useSWR` 进行联动从而降低维护成本:
@ -342,9 +342,9 @@ const Demo = () => {
`4.22.0` 新增,可用于获取当前 Form.Item 的校验状态,如果上层没有 Form.Item`status` 将会返回 `undefined`
```tsx
const CustomInput = ({ value }) => {
const CustomInput = ({ value, onChange }) => {
const { status } = Form.Item.useStatus();
return <input value={value} className={`custom-input-${status}`} />;
return <input value={value} onChange={onChange} className={`custom-input-${status}`} />;
};
export default () => (

View File

@ -8,6 +8,7 @@ exports[`renders ./components/image/demo/basic.md extend context correctly 1`] =
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="200"
/>
<div
class="ant-image-mask"
@ -58,9 +59,11 @@ Array [
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
style="display:none"
width="200"
/>
<div
class="ant-image-mask"
style="display:none"
>
<div
class="ant-image-mask-info"
@ -98,8 +101,10 @@ exports[`renders ./components/image/demo/fallback.md extend context correctly 1`
>
<img
class="ant-image-img"
height="200"
src="error"
style="height:200px"
width="200"
/>
<div
class="ant-image-mask"
@ -147,6 +152,7 @@ exports[`renders ./components/image/demo/placeholder.md extend context correctly
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?undefined"
width="200"
/>
<div
aria-hidden="true"
@ -159,6 +165,7 @@ exports[`renders ./components/image/demo/placeholder.md extend context correctly
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
width="200"
/>
</div>
</div>
@ -216,6 +223,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="200"
/>
<div
class="ant-image-mask"
@ -253,6 +261,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
width="200"
/>
<div
class="ant-image-mask"
@ -295,6 +304,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="150"
/>
<div
class="ant-image-mask"
@ -332,6 +342,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
width="150"
/>
<div
class="ant-image-mask"
@ -369,6 +380,7 @@ Array [
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="150"
/>
<div
class="ant-image-mask"
@ -411,6 +423,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
width="200"
/>
<div
class="ant-image-mask"
@ -564,6 +577,7 @@ exports[`renders ./components/image/demo/preview-mask.md extend context correctl
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="96"
/>
<div
class="ant-image-mask customize-mask"
@ -613,6 +627,7 @@ exports[`renders ./components/image/demo/previewSrc.md extend context correctly
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
width="200"
/>
<div
class="ant-image-mask"

View File

@ -8,6 +8,7 @@ exports[`renders ./components/image/demo/basic.md correctly 1`] = `
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="200"
/>
<div
class="ant-image-mask"
@ -58,9 +59,11 @@ Array [
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
style="display:none"
width="200"
/>
<div
class="ant-image-mask"
style="display:none"
>
<div
class="ant-image-mask-info"
@ -98,8 +101,10 @@ exports[`renders ./components/image/demo/fallback.md correctly 1`] = `
>
<img
class="ant-image-img"
height="200"
src="error"
style="height:200px"
width="200"
/>
<div
class="ant-image-mask"
@ -147,6 +152,7 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = `
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?undefined"
width="200"
/>
<div
aria-hidden="true"
@ -159,6 +165,7 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = `
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
width="200"
/>
</div>
</div>
@ -216,6 +223,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="200"
/>
<div
class="ant-image-mask"
@ -253,6 +261,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
width="200"
/>
<div
class="ant-image-mask"
@ -295,6 +304,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="150"
/>
<div
class="ant-image-mask"
@ -332,6 +342,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
width="150"
/>
<div
class="ant-image-mask"
@ -369,6 +380,7 @@ Array [
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="150"
/>
<div
class="ant-image-mask"
@ -411,6 +423,7 @@ Array [
<img
class="ant-image-img"
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
width="200"
/>
<div
class="ant-image-mask"
@ -564,6 +577,7 @@ exports[`renders ./components/image/demo/preview-mask.md correctly 1`] = `
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
width="96"
/>
<div
class="ant-image-mask customize-mask"
@ -613,6 +627,7 @@ exports[`renders ./components/image/demo/previewSrc.md correctly 1`] = `
<img
class="ant-image-img"
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
width="200"
/>
<div
class="ant-image-mask"

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/az_AZ';
import Calendar from '../calendar/locale/az_AZ';
import DatePicker from '../date-picker/locale/az_AZ';
import type { Locale } from '../locale-provider';
import TimePicker from '../time-picker/locale/az_AZ';
const typeTemplate = '${label}Hökmlü deyil${type}';
const localeValues: Locale = {
locale: 'az',
Pagination,
@ -43,6 +46,56 @@ const localeValues: Locale = {
uploadError: 'Yükləmə xətası',
previewFile: 'Fayla önbaxış',
},
Form: {
optional: 'Seçimli',
defaultValidateMessages: {
default: 'Sahə təsdiq xətası${label}',
required: 'Xahiş edirik daxil olun${label}',
enum: '${label}Onlardan biri olmalıdır[${enum}]',
whitespace: '${label}Null xarakter ola bilməz',
date: {
format: '${label}Tarix formatı hökmlü deyil',
parse: '${label}Tarixi döndərmək mümkün deyil',
invalid: '${label}səhv tarixdir',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label}Olmalıdır${len}işarələr',
min: '${label}ən az${min}işarələr',
max: '${label}ən çox${max}işarələr',
range: '${label}Olmalıdır${min}-${max}hərflər arasında',
},
number: {
len: '${label}Bərabər olmalıdır${len}',
min: '${label}Minimal dəyəri${min}',
max: '${label}Maksimal qiymət:${max}',
range: '${label}Olmalıdır${min}-${max}aralarında',
},
array: {
len: 'Olmalıdır${len}parça${label}',
min: 'ən az${min}parça${label}',
max: 'ən çox${max}parça${label}',
range: '${label}miqdarıOlmalıdır${min}-${max}aralarında',
},
pattern: {
mismatch: '${label}Şablona uyğun gəlmir${pattern}',
},
},
},
};
export default localeValues;

View File

@ -1,97 +0,0 @@
import { mount } from 'enzyme';
import React from 'react';
import Mentions from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act } from '../../../tests/utils';
const { getMentions } = Mentions;
function simulateInput(wrapper, text, keyEvent) {
const lastChar = text[text.length - 1];
const myKeyEvent = keyEvent || {
which: lastChar.charCodeAt(0),
key: lastChar,
target: {
value: text,
selectionStart: text.length,
},
};
wrapper.find('textarea').simulate('keyDown', myKeyEvent);
const textareaInstance = wrapper.find('textarea').instance();
textareaInstance.value = text;
textareaInstance.selectionStart = text.length;
textareaInstance.selectionStart = text.length;
if (!keyEvent) {
wrapper.find('textarea').simulate('change', {
target: { value: text },
});
}
wrapper.find('textarea').simulate('keyUp', myKeyEvent);
wrapper.update();
}
describe('Mentions', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('getMentions', () => {
const mentions = getMentions('@light #bamboo cat', { prefix: ['@', '#'] });
expect(mentions).toEqual([
{
prefix: '@',
value: 'light',
},
{
prefix: '#',
value: 'bamboo',
},
]);
});
it('focus', () => {
const onFocus = jest.fn();
const onBlur = jest.fn();
const wrapper = mount(<Mentions onFocus={onFocus} onBlur={onBlur} />);
wrapper.find('textarea').simulate('focus');
expect(wrapper.find('.ant-mentions').hasClass('ant-mentions-focused')).toBeTruthy();
expect(onFocus).toHaveBeenCalled();
wrapper.find('textarea').simulate('blur');
act(() => {
jest.runAllTimers();
});
wrapper.update();
expect(wrapper.find('.ant-mentions').hasClass('ant-mentions-focused')).toBeFalsy();
expect(onBlur).toHaveBeenCalled();
});
focusTest(Mentions, { refFocus: true });
mountTest(Mentions);
rtlTest(Mentions);
it('loading', () => {
const wrapper = mount(<Mentions loading />);
simulateInput(wrapper, '@');
expect(wrapper.find('li.ant-mentions-dropdown-menu-item').length).toBe(1);
expect(wrapper.find('.ant-spin').length).toBeTruthy();
});
it('notFoundContent', () => {
const wrapper = mount(<Mentions notFoundContent={<span className="bamboo-light" />} />);
simulateInput(wrapper, '@');
expect(wrapper.find('li.ant-mentions-dropdown-menu-item').length).toBe(1);
expect(wrapper.find('.bamboo-light').length).toBeTruthy();
});
});

View File

@ -0,0 +1,87 @@
import React from 'react';
import Mentions from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, render, fireEvent } from '../../../tests/utils';
const { getMentions } = Mentions;
function simulateInput(wrapper: ReturnType<typeof render>, text: string, keyEvent?: Event): void {
const lastChar = text[text.length - 1];
const myKeyEvent = keyEvent || {
which: lastChar.charCodeAt(0),
key: lastChar,
target: {
value: text,
selectionStart: text.length,
},
};
fireEvent.keyDown(wrapper.container.querySelector('textarea')!, myKeyEvent);
const textareaInstance = wrapper.container.querySelector('textarea');
if (textareaInstance) {
textareaInstance.value = text;
textareaInstance.selectionStart = text.length;
textareaInstance.selectionStart = text.length;
}
if (!keyEvent) {
fireEvent.change(wrapper.container.querySelector('textarea')!, { target: { value: text } });
}
fireEvent.keyUp(wrapper.container.querySelector('textarea')!, myKeyEvent);
}
describe('Mentions', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('getMentions', () => {
const mentions = getMentions('@light #bamboo cat', { prefix: ['@', '#'] });
expect(mentions).toEqual([
{ prefix: '@', value: 'light' },
{ prefix: '#', value: 'bamboo' },
]);
});
it('focus', () => {
const onFocus = jest.fn();
const onBlur = jest.fn();
const { container } = render(<Mentions onFocus={onFocus} onBlur={onBlur} />);
fireEvent.focus(container.querySelector('textarea')!);
expect(container.querySelector('.ant-mentions')).toHaveClass('ant-mentions-focused');
expect(onFocus).toHaveBeenCalled();
fireEvent.blur(container.querySelector('textarea')!);
act(() => {
jest.runAllTimers();
});
expect(container.querySelector('.ant-mentions')).not.toHaveClass('ant-mentions-focused');
expect(onBlur).toHaveBeenCalled();
});
focusTest(Mentions, { refFocus: true });
mountTest(Mentions);
rtlTest(Mentions);
it('loading', () => {
const wrapper = render(<Mentions loading />);
simulateInput(wrapper, '@');
expect(wrapper.container.querySelectorAll('li.ant-mentions-dropdown-menu-item').length).toBe(1);
expect(wrapper.container.querySelectorAll('.ant-spin').length).toBeTruthy();
});
it('notFoundContent', () => {
const wrapper = render(<Mentions notFoundContent={<span className="bamboo-light" />} />);
simulateInput(wrapper, '@');
expect(wrapper.container.querySelectorAll('li.ant-mentions-dropdown-menu-item').length).toBe(1);
expect(wrapper.container.querySelectorAll('.bamboo-light').length).toBeTruthy();
});
});

View File

@ -1,237 +1,277 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import message, { actWrapper } from '..';
import { act, fireEvent, sleep } from '../../../tests/utils';
import { awaitPromise, triggerMotionEnd } from './util';
import message, { getInstance, type MessageType } from '..';
import { act, render, fireEvent } from '../../../tests/utils';
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
writable: true,
value: true,
});
describe('message', () => {
beforeAll(() => {
actWrapper(act);
});
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(async () => {
// Clean up
message.destroy();
await triggerMotionEnd();
act(() => {
jest.runAllTimers();
});
afterEach(() => {
jest.useRealTimers();
await awaitPromise();
act(() => {
message.destroy();
});
});
it('should be able to hide manually', async () => {
const hide1 = message.info('whatever', 0);
const hide2 = message.info('whatever', 0);
let hide1!: MessageType;
let hide2!: MessageType;
await awaitPromise();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
act(() => {
hide1 = message.info('whatever', 0);
});
act(() => {
hide2 = message.info('whatever', 0);
});
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
hide1();
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
act(() => {
jest.runAllTimers();
});
expect(getInstance()!.component.state.notices).toHaveLength(1);
hide2();
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
act(() => {
jest.runAllTimers();
});
expect(getInstance()!.component.state.notices).toHaveLength(0);
});
it('should be able to remove manually with a unique key', async () => {
it('should be able to remove manually with a unique key', () => {
const key1 = 'key1';
const key2 = 'key2';
message.info({ content: 'Message1', key: 'key1', duration: 0 });
message.info({ content: 'Message2', key: 'key2', duration: 0 });
act(() => {
message.info({ content: 'Message1', key: 'key1', duration: 0 });
});
act(() => {
message.info({ content: 'Message2', key: 'key2', duration: 0 });
});
await awaitPromise();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
message.destroy(key1);
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
act(() => {
jest.runAllTimers();
});
expect(getInstance()!.component.state.notices).toHaveLength(1);
message.destroy(key2);
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
act(() => {
jest.runAllTimers();
});
expect(getInstance()!.component.state.notices).toHaveLength(0);
});
it('should be able to destroy globally', async () => {
message.info('whatever', 0);
message.info('whatever', 0);
it('should be able to destroy globally', () => {
act(() => {
message.info('whatever', 0);
});
act(() => {
message.info('whatever', 0);
});
await awaitPromise();
expect(document.querySelectorAll('.ant-message')).toHaveLength(1);
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
expect(document.querySelectorAll('.ant-message').length).toBe(1);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
message.destroy();
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message')).toHaveLength(0);
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
});
it('should not need to use duration argument when using the onClose arguments', async () => {
const onClose = jest.fn();
const close = message.info('whatever', onClose);
await awaitPromise();
close();
await triggerMotionEnd();
expect(onClose).toHaveBeenCalled();
});
it('should have the default duration when using the onClose arguments', async () => {
const onClose = jest.fn();
message.info('whatever', onClose);
await awaitPromise();
act(() => {
jest.advanceTimersByTime(2500);
jest.runAllTimers();
});
expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(document.querySelector('.ant-message-move-up-leave')).toBeTruthy();
expect(document.querySelectorAll('.ant-message').length).toBe(0);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
});
it('trigger onClick method', async () => {
it('should not need to use duration argument when using the onClose arguments', () => {
message.info('whatever', () => {});
});
it('should have the default duration when using the onClose arguments', done => {
jest.useRealTimers();
const defaultDuration = 3;
const now = Date.now();
message.info('whatever', () => {
// calculate the approximately duration value
const aboutDuration = parseInt(String((Date.now() - now) / 1000), 10);
expect(aboutDuration).toBe(defaultDuration);
done();
});
});
it('trigger onClick method', () => {
const onClick = jest.fn();
message.info({
onClick,
duration: 0,
content: 'message info',
});
class Test extends React.Component {
static isFirstRender = true;
await awaitPromise();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
fireEvent.click(document.querySelector('.ant-message-notice')!);
componentDidMount() {
if (Test.isFirstRender) {
Test.isFirstRender = false;
message.info({
onClick,
duration: 0,
content: 'message info',
});
}
}
render() {
return <div>test message onClick method</div>;
}
}
render(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
fireEvent.click(document.querySelectorAll('.ant-message-notice')[0]);
expect(onClick).toHaveBeenCalled();
});
it('should be called like promise', async () => {
const onClose = jest.fn();
message.info('whatever').then(onClose);
await awaitPromise();
act(() => {
jest.advanceTimersByTime(2500);
it('should be called like promise', done => {
jest.useRealTimers();
const defaultDuration = 3;
const now = Date.now();
message.info('whatever').then(() => {
// calculate the approximately duration value
const aboutDuration = parseInt(String((Date.now() - now) / 1000), 10);
expect(aboutDuration).toBe(defaultDuration);
done();
});
expect(onClose).not.toHaveBeenCalled();
act(() => {
jest.advanceTimersByTime(1000);
});
await sleep(); // Wait to let event loop run
expect(onClose).toHaveBeenCalled();
});
// https://github.com/ant-design/ant-design/issues/8201
it('should hide message correctly', async () => {
const hide = message.loading('Action in progress..', 0);
await awaitPromise();
it('should hide message correctly', () => {
let hide!: MessageType;
class Test extends React.Component {
static isFirstRender = true;
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
componentDidMount() {
act(() => {
if (Test.isFirstRender) {
Test.isFirstRender = false;
hide = message.loading('Action in progress..', 0);
}
});
}
hide!();
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
render() {
return <div>test</div>;
}
}
render(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
act(() => {
hide();
jest.runAllTimers();
});
expect(getInstance()!.component.state.notices).toHaveLength(0);
});
it('should allow custom icon', async () => {
message.open({ content: 'Message', icon: <SmileOutlined /> });
await awaitPromise();
expect(document.querySelector('.anticon-smile')).toBeTruthy();
it('should allow custom icon', () => {
act(() => {
message.open({ content: 'Message', icon: <SmileOutlined /> });
});
expect(document.querySelectorAll('.anticon-smile').length).toBe(1);
});
it('should have no icon', async () => {
it('should have no icon', () => {
message.open({ content: 'Message', icon: <span /> });
await awaitPromise();
expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
});
it('should have no icon when not pass icon props', async () => {
it('should have no icon when not pass icon props', () => {
message.open({ content: 'Message' });
await awaitPromise();
expect(document.querySelector('.ant-message-notice .anticon')).toBeFalsy();
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
});
// https://github.com/ant-design/ant-design/issues/8201
it('should destroy messages correctly', async () => {
message.loading('Action in progress1..', 0);
message.loading('Action in progress2..', 0);
setTimeout(() => message.destroy(), 1000);
await awaitPromise();
it('should destroy messages correctly', () => {
class Test extends React.Component {
static isFirstRender = true;
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(2);
componentDidMount() {
if (Test.isFirstRender) {
Test.isFirstRender = false;
message.loading('Action in progress1..', 0);
message.loading('Action in progress2..', 0);
setTimeout(() => message.destroy(), 1000);
}
}
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
render() {
return <div>test</div>;
}
}
render(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
act(() => {
jest.runAllTimers();
});
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
});
it('should support update message content with a unique key', async () => {
it('should support update message content with a unique key', () => {
const key = 'updatable';
class Test extends React.Component {
componentDidMount() {
message.loading({ content: 'Loading...', key });
// Testing that content of the message should be updated.
setTimeout(() => message.success({ content: 'Loaded', key }), 1000);
setTimeout(() => message.destroy(), 3000);
}
message.loading({ content: 'Loading...', key });
// Testing that content of the message should be updated.
setTimeout(() => message.success({ content: 'Loaded', key }), 1000);
setTimeout(() => message.destroy(), 3000);
await awaitPromise();
render() {
return <div>test</div>;
}
}
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
render(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
act(() => {
jest.advanceTimersByTime(1500);
});
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
expect(document.querySelector('.ant-message-move-up-leave')).toBeFalsy();
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(0);
});
it('update message content with a unique key and cancel manually', async () => {
const key = 'updatable';
const hideLoading = message.loading({ content: 'Loading...', key, duration: 0 });
await awaitPromise();
setTimeout(() => {
act(() => {
hideLoading();
});
}, 1000);
expect(document.querySelectorAll('.ant-message-notice')).toHaveLength(1);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
act(() => {
jest.advanceTimersByTime(1500);
jest.runAllTimers();
});
expect(document.querySelectorAll('.ant-message-move-up-leave')).toHaveLength(1);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
});
it('should not throw error when pass null', async () => {
it('update message content with a unique key and cancel manually', () => {
const key = 'updatable';
class Test extends React.Component {
componentDidMount() {
let hideLoading: MessageType;
act(() => {
hideLoading = message.loading({ content: 'Loading...', key, duration: 0 });
});
// Testing that content of the message should be cancel manually.
setTimeout(() => {
act(() => {
hideLoading();
});
}, 1000);
}
render() {
return <div>test</div>;
}
}
render(<Test />);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
jest.advanceTimersByTime(1500);
expect(getInstance()!.component.state.notices).toHaveLength(0);
});
it('should not throw error when pass null', () => {
message.error(null);
await awaitPromise();
});
});

View File

@ -1,292 +1,339 @@
import React from 'react';
import { UserOutlined } from '@ant-design/icons';
import notification, { actWrapper } from '..';
import notification, { getInstance, type NotificationInstance } from '..';
import { sleep, act } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import { act, fireEvent } from '../../../tests/utils';
import { awaitPromise, triggerMotionEnd } from './util';
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
writable: true,
value: true,
});
type NotificationWithIconType = keyof Omit<NotificationInstance, 'open'>;
describe('notification', () => {
beforeAll(() => {
actWrapper(act);
});
beforeEach(() => {
jest.useFakeTimers();
jest.clearAllTimers();
});
afterEach(async () => {
// Clean up
notification.destroy();
await triggerMotionEnd();
notification.config({
prefixCls: null,
getContainer: null,
afterEach(() => {
act(() => {
jest.runAllTimers();
});
jest.useRealTimers();
await awaitPromise();
act(() => {
notification.destroy();
});
});
it('not duplicate create holder', async () => {
notification.config({
prefixCls: 'additional-holder',
});
for (let i = 0; i < 5; i += 1) {
notification.open({
message: 'Notification Title',
duration: 0,
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
prefixCls: 'additional-holder',
});
});
}
await awaitPromise();
await sleep();
const count = document.querySelectorAll('.additional-holder').length;
expect(count).toEqual(1);
});
it('should be able to hide manually', async () => {
act(() => {
notification.open({
message: 'Notification Title 1',
duration: 0,
key: '1',
});
jest.runAllTimers();
});
act(() => {
jest.runAllTimers();
});
expect(document.querySelectorAll('.additional-holder')).toHaveLength(1);
});
it('should be able to hide manually', async () => {
notification.open({
message: 'Notification Title 1',
duration: 0,
key: '1',
});
await awaitPromise();
notification.open({
message: 'Notification Title 2',
duration: 0,
key: '2',
act(() => {
notification.open({
message: 'Notification Title 2',
duration: 0,
key: '2',
});
jest.runAllTimers();
});
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(2);
act(() => {
jest.runAllTimers();
});
// Close 1
notification.destroy('1');
await act(async () => {
await Promise.resolve();
});
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
await triggerMotionEnd();
act(() => {
notification.close('1');
jest.runAllTimers();
});
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(1);
await act(async () => {
await Promise.resolve();
});
expect((await getInstance('ant-notification-topRight'))!.component.state.notices).toHaveLength(
1,
);
// Close 2
notification.destroy('2');
act(() => {
notification.close('2');
jest.runAllTimers();
});
await triggerMotionEnd();
await act(async () => {
await Promise.resolve();
});
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect((await getInstance('ant-notification-topRight'))!.component.state.notices).toHaveLength(
0,
);
});
it('should be able to destroy globally', async () => {
notification.open({
message: 'Notification Title 1',
duration: 0,
});
await awaitPromise();
notification.open({
message: 'Notification Title 2',
duration: 0,
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
});
});
expect(document.querySelectorAll('.ant-notification')).toHaveLength(1);
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(2);
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
});
});
notification.destroy();
await act(async () => {
await Promise.resolve();
});
await triggerMotionEnd();
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
expect(document.querySelectorAll('.ant-notification')).toHaveLength(0);
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
act(() => {
notification.destroy();
});
await act(async () => {
await Promise.resolve();
});
expect(document.querySelectorAll('.ant-notification').length).toBe(0);
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0);
});
it('should be able to destroy after config', () => {
notification.config({
bottom: 100,
act(() => {
notification.config({
bottom: 100,
});
});
notification.destroy();
act(() => {
notification.destroy();
});
});
it('should be able to config rtl', async () => {
notification.config({
rtl: true,
it('should be able to config rtl', () => {
act(() => {
notification.config({
rtl: true,
});
});
notification.open({
message: 'whatever',
act(() => {
notification.open({
message: 'whatever',
});
});
await awaitPromise();
expect(document.querySelectorAll('.ant-notification-rtl')).toHaveLength(1);
expect(document.querySelectorAll('.ant-notification-rtl').length).toBe(1);
});
it('should be able to global config rootPrefixCls', async () => {
it('should be able to global config rootPrefixCls', () => {
ConfigProvider.config({ prefixCls: 'prefix-test', iconPrefixCls: 'bamboo' });
notification.success({ message: 'Notification Title', duration: 0 });
await awaitPromise();
act(() => {
notification.success({ message: 'Notification Title', duration: 0 });
});
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notification-notice')).toHaveLength(1);
expect(document.querySelectorAll('.bamboo-check-circle')).toHaveLength(1);
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: null! });
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: '' });
});
it('should be able to config prefixCls', async () => {
it('should be able to config prefixCls', () => {
notification.config({
prefixCls: 'prefix-test',
});
notification.open({
message: 'Notification Title',
duration: 0,
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
});
});
await awaitPromise();
expect(document.querySelectorAll('.ant-notification-notice')).toHaveLength(0);
expect(document.querySelectorAll('.prefix-test-notice')).toHaveLength(1);
notification.config({
prefixCls: null,
prefixCls: '',
});
});
it('should be able to open with icon', async () => {
const iconPrefix = '.ant-notification-notice-icon';
const list = ['success', 'info', 'warning', 'error'] as const;
list.forEach(type => {
notification[type]({
message: 'Notification Title',
duration: 0,
description: 'This is the content of the notification.',
const openNotificationWithIcon = async (type: NotificationWithIconType) => {
act(() => {
notification[type]({
message: 'Notification Title',
duration: 0,
description: 'This is the content of the notification.',
});
jest.runAllTimers();
});
};
const list: Array<NotificationWithIconType> = ['success', 'info', 'warning', 'error'];
const promises = list.map(type => openNotificationWithIcon(type));
await act(async () => {
await Promise.all(promises);
});
await awaitPromise();
list.forEach(type => {
expect(document.querySelectorAll(`${iconPrefix}-${type}`)).toHaveLength(1);
expect(document.querySelectorAll(`${iconPrefix}-${type}`).length).toBe(1);
});
});
it('should be able to add parent class for different notification types', async () => {
const list = ['success', 'info', 'warning', 'error'] as const;
const openNotificationWithIcon = async (type: NotificationWithIconType) => {
act(() => {
notification[type]({
message: 'Notification Title',
duration: 0,
description: 'This is the content of the notification.',
});
jest.runAllTimers();
});
};
const list: Array<NotificationWithIconType> = ['success', 'info', 'warning', 'error'];
const promises = list.map(type => openNotificationWithIcon(type));
await act(async () => {
await Promise.all(promises);
});
list.forEach(type => {
notification[type]({
expect(document.querySelectorAll(`.ant-notification-notice-${type}`).length).toBe(1);
});
});
it('trigger onClick', () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
description: 'This is the content of the notification.',
});
});
await awaitPromise();
list.forEach(type => {
expect(document.querySelectorAll(`.ant-notification-notice-${type}`)).toHaveLength(1);
});
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
});
it('trigger onClick', async () => {
const onClick = jest.fn();
notification.open({
message: 'Notification Title',
duration: 0,
onClick,
it('support closeIcon', () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: <span className="test-customize-icon" />,
});
});
await awaitPromise();
expect(document.querySelectorAll('.ant-notification')).toHaveLength(1);
fireEvent.click(document.querySelector('.ant-notification-notice')!);
expect(onClick).toHaveBeenCalled();
expect(document.querySelectorAll('.test-customize-icon').length).toBe(1);
});
it('support closeIcon', async () => {
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: <span className="test-customize-icon" />,
});
await awaitPromise();
expect(document.querySelectorAll('.test-customize-icon')).toHaveLength(1);
});
it('support config closeIcon', async () => {
it('support config closeIcon', () => {
notification.config({
closeIcon: <span className="test-customize-icon" />,
});
// Global Icon
notification.open({
message: 'Notification Title',
duration: 0,
});
await awaitPromise();
expect(document.querySelector('.test-customize-icon')).toBeTruthy();
// Notice Icon
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: <span className="replace-icon" />,
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
closeIcon: <span className="test-customize-icon" />,
});
});
expect(document.querySelector('.replace-icon')).toBeTruthy();
notification.config({
closeIcon: null,
});
expect(document.querySelectorAll('.test-customize-icon').length).toBe(1);
});
it('closeIcon should be update', async () => {
const list = ['1', '2'];
list.forEach(type => {
notification.open({
message: 'Notification Title',
closeIcon: <span className={`test-customize-icon-${type}`} />,
duration: 0,
const openNotificationWithCloseIcon = async (type: '1' | '2') => {
act(() => {
notification.open({
message: 'Notification Title',
closeIcon: <span className={`test-customize-icon-${type}`} />,
});
jest.runAllTimers();
});
};
const list: Array<'1' | '2'> = ['1', '2'];
const promises = list.map(type => openNotificationWithCloseIcon(type));
await act(async () => {
await Promise.all(promises);
});
await awaitPromise();
list.forEach(type => {
expect(document.querySelector(`.test-customize-icon-${type}`)).toBeTruthy();
expect(document.querySelectorAll(`.test-customize-icon-${type}`).length).toBe(1);
});
});
it('support config duration', async () => {
it('support config duration', () => {
notification.config({
duration: 0,
});
notification.open({
message: 'whatever',
act(() => {
notification.open({
message: 'whatever',
});
});
await awaitPromise();
expect(document.querySelector('.ant-notification')).toBeTruthy();
expect(document.querySelectorAll('.ant-notification').length).toBe(1);
});
it('support icon', async () => {
notification.open({
message: 'Notification Title',
duration: 0,
icon: <UserOutlined />,
it('support icon', () => {
act(() => {
notification.open({
message: 'Notification Title',
duration: 0,
icon: <UserOutlined />,
});
});
await awaitPromise();
expect(document.querySelector('.anticon-user')).toBeTruthy();
expect(document.querySelectorAll('.anticon-user').length).toBe(1);
});
});

View File

@ -1,40 +1,39 @@
import { mount } from 'enzyme';
import React from 'react';
import Popover from '..';
import mountTest from '../../../tests/shared/mountTest';
import { render } from '../../../tests/utils';
import { render, fireEvent } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
describe('Popover', () => {
mountTest(Popover);
it('should show overlay when trigger is clicked', () => {
const ref = React.createRef();
const ref = React.createRef<any>();
const popover = mount(
const popover = render(
<Popover ref={ref} content="console.log('hello world')" title="code" trigger="click">
<span>show me your code</span>
</Popover>,
);
expect(ref.current.getPopupDomNode()).toBe(null);
popover.find('span').simulate('click');
expect(popover.find('Trigger PopupInner').props().visible).toBeTruthy();
expect(popover.container.querySelector('.ant-popover-inner-content')).toBeFalsy();
fireEvent.click(popover.container.querySelector('span')!);
expect(popover.container.querySelector('.ant-popover-inner-content')).toBeTruthy();
});
it('shows content for render functions', () => {
const renderTitle = () => 'some-title';
const renderContent = () => 'some-content';
const ref = React.createRef();
const ref = React.createRef<any>();
const popover = mount(
const popover = render(
<Popover ref={ref} content={renderContent} title={renderTitle} trigger="click">
<span>show me your code</span>
<span>show me your code </span>
</Popover>,
);
popover.find('span').simulate('click');
fireEvent.click(popover.container.querySelector('span')!);
const popup = ref.current.getPopupDomNode();
expect(popup).not.toBe(null);
@ -44,30 +43,31 @@ describe('Popover', () => {
});
it('handles empty title/content props safely', () => {
const ref = React.createRef();
const ref = React.createRef<any>();
const popover = mount(
const popover = render(
<Popover trigger="click" ref={ref}>
<span>show me your code</span>
</Popover>,
);
popover.find('span').simulate('click');
fireEvent.click(popover.container.querySelector('span')!);
const popup = ref.current.getPopupDomNode();
expect(popup).toBe(null);
});
it('should not render popover when the title & content props is empty', () => {
const ref = React.createRef();
const ref = React.createRef<any>();
const popover = mount(
const popover = render(
<Popover trigger="click" ref={ref} content="">
<span>show me your code</span>
</Popover>,
);
popover.find('span').simulate('click');
fireEvent.click(popover.container.querySelector('span')!);
const popup = ref.current.getPopupDomNode();
expect(popup).toBe(null);
@ -88,13 +88,13 @@ describe('Popover', () => {
});
it(`should be rendered correctly in RTL direction`, () => {
const wrapper = mount(
const wrapper = render(
<ConfigProvider direction="rtl">
<Popover title="RTL" visible>
<span>show me your Rtl demo</span>
</Popover>
</ConfigProvider>,
);
expect(wrapper.render()).toMatchSnapshot();
expect(Array.from(wrapper.container.children)).toMatchSnapshot();
});
});

View File

@ -59,6 +59,7 @@ const genStatusStyle = (
rootSelectCls: string,
token: {
componentCls: string;
antCls: string;
borderHoverColor: string;
outlineColor: string;
controlOutlineWidth: number;
@ -66,7 +67,7 @@ const genStatusStyle = (
},
overwriteDefaultBorder: boolean = false,
): CSSObject => {
const { componentCls, borderHoverColor, outlineColor } = token;
const { componentCls, borderHoverColor, outlineColor, antCls } = token;
const overwriteStyle: CSSObject = overwriteDefaultBorder
? {
@ -78,21 +79,22 @@ const genStatusStyle = (
return {
[rootSelectCls]: {
[`&:not(${componentCls}-disabled):not(${componentCls}-customize-input)`]: {
...overwriteStyle,
[`&:not(${componentCls}-disabled):not(${componentCls}-customize-input):not(${antCls}-pagination-size-changer)`]:
{
...overwriteStyle,
[`${componentCls}-focused& ${componentCls}-selector`]: {
borderColor: borderHoverColor,
boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${outlineColor}`,
borderInlineEndWidth: `${token.controlLineWidth}px !important`,
outline: 0,
},
[`${componentCls}-focused& ${componentCls}-selector`]: {
borderColor: borderHoverColor,
boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${outlineColor}`,
borderInlineEndWidth: `${token.controlLineWidth}px !important`,
outline: 0,
},
[`&:hover ${componentCls}-selector`]: {
borderColor: borderHoverColor,
borderInlineEndWidth: `${token.controlLineWidth}px !important`,
[`&:hover ${componentCls}-selector`]: {
borderColor: borderHoverColor,
borderInlineEndWidth: `${token.controlLineWidth}px !important`,
},
},
},
},
};
};

View File

@ -844,6 +844,435 @@ exports[`renders ./components/steps/demo/icon.md extend context correctly 1`] =
</div>
`;
exports[`renders ./components/steps/demo/label-placement.md extend context correctly 1`] = `
Array [
<div
class="ant-steps ant-steps-horizontal ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
2
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
<br />,
<div
class="ant-steps ant-steps-horizontal ant-steps-with-progress ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<div
class="ant-steps-progress-icon"
>
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
<div
class="ant-progress-inner"
style="width:40px;height:40px;font-size:12px"
>
<svg
class="ant-progress-circle"
viewBox="0 0 100 100"
>
<circle
class="ant-progress-circle-trail"
cx="50"
cy="50"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:0;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
<circle
class="ant-progress-circle-path"
cx="50"
cy="50"
opacity="1"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:122.63715789784806;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
<circle
class="ant-progress-circle-path"
cx="50"
cy="50"
opacity="0"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke:#52C41A;stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:301.58289474462015;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
</svg>
<span
class="ant-progress-text"
/>
</div>
</div>
<span
class="ant-steps-icon"
>
2
</span>
</div>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
<br />,
<div
class="ant-steps ant-steps-horizontal ant-steps-small ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
2
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/steps/demo/nav.md extend context correctly 1`] = `
Array [
<div

View File

@ -724,6 +724,435 @@ exports[`renders ./components/steps/demo/icon.md correctly 1`] = `
</div>
`;
exports[`renders ./components/steps/demo/label-placement.md correctly 1`] = `
Array [
<div
class="ant-steps ant-steps-horizontal ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
2
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
<br />,
<div
class="ant-steps ant-steps-horizontal ant-steps-with-progress ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<div
class="ant-steps-progress-icon"
>
<div
class="ant-progress ant-progress-circle ant-progress-status-normal ant-progress-show-info ant-progress-default"
>
<div
class="ant-progress-inner"
style="width:40px;height:40px;font-size:12px"
>
<svg
class="ant-progress-circle"
viewBox="0 0 100 100"
>
<circle
class="ant-progress-circle-trail"
cx="50"
cy="50"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:0;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
<circle
class="ant-progress-circle-path"
cx="50"
cy="50"
opacity="1"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:122.63715789784806;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
<circle
class="ant-progress-circle-path"
cx="50"
cy="50"
opacity="0"
r="48"
stroke-linecap="round"
stroke-width="4"
style="stroke:#52C41A;stroke-dasharray:301.59289474462014px 301.59289474462014;stroke-dashoffset:301.58289474462015;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
/>
</svg>
<span
class="ant-progress-text"
/>
</div>
</div>
<span
class="ant-steps-icon"
>
2
</span>
</div>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
<br />,
<div
class="ant-steps ant-steps-horizontal ant-steps-small ant-steps-label-vertical"
>
<div
class="ant-steps-item ant-steps-item-finish"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
<span
aria-label="check"
class="anticon anticon-check ant-steps-finish-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Finished
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-process ant-steps-item-active"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
2
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
In Progress
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
<div
class="ant-steps-item ant-steps-item-wait"
>
<div
class="ant-steps-item-container"
>
<div
class="ant-steps-item-tail"
/>
<div
class="ant-steps-item-icon"
>
<span
class="ant-steps-icon"
>
3
</span>
</div>
<div
class="ant-steps-item-content"
>
<div
class="ant-steps-item-title"
>
Waiting
</div>
<div
class="ant-steps-item-description"
>
This is a description.
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/steps/demo/nav.md correctly 1`] = `
Array [
<div

View File

@ -0,0 +1,45 @@
---
order: 14
title:
zh-CN: 标签放置位置
en-US: Label Placement
---
## zh-CN
修改标签放置位置为 `vertical`
## en-US
Set labelPlacement to `vertical`.
```tsx
import { Steps } from 'antd';
import React from 'react';
const { Step } = Steps;
const App: React.FC = () => (
<>
<Steps current={1} labelPlacement="vertical">
<Step title="Finished" description="This is a description." />
<Step title="In Progress" description="This is a description." />
<Step title="Waiting" description="This is a description." />
</Steps>
<br />
<Steps current={1} percent={60} labelPlacement="vertical">
<Step title="Finished" description="This is a description." />
<Step title="In Progress" description="This is a description." />
<Step title="Waiting" description="This is a description." />
</Steps>
<br />
<Steps current={1} size="small" labelPlacement="vertical">
<Step title="Finished" description="This is a description." />
<Step title="In Progress" description="This is a description." />
<Step title="Waiting" description="This is a description." />
</Steps>
</>
);
export default App;
```

View File

@ -15,9 +15,17 @@ const genStepsProgressStyle: GenerateStyle<StepsToken, CSSObject> = token => {
},
},
[`&${componentCls}-horizontal ${componentCls}-item:first-child`]: {
paddingBottom: token.paddingXXS,
paddingInlineStart: token.paddingXXS,
[`&${componentCls}-horizontal`]: {
[`${componentCls}-item:first-child`]: {
paddingBottom: token.paddingXXS,
paddingInlineStart: token.paddingXXS,
},
},
[`&${componentCls}-label-vertical`]: {
[`${componentCls}-item ${componentCls}-item-tail`]: {
top: token.margin - 2 * token.lineWidth,
},
},
[`${componentCls}-item-icon`]: {

View File

@ -3,6 +3,7 @@ import type { UploadProps as RcUploadProps } from 'rc-upload';
import RcUpload from 'rc-upload';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import * as React from 'react';
import { flushSync } from 'react-dom';
import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
@ -103,7 +104,11 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
cloneList = cloneList.slice(0, maxCount);
}
setMergedFileList(cloneList);
// Prevent React18 auto batch since input[upload] trigger process at same time
// which makes fileList closure problem
flushSync(() => {
setMergedFileList(cloneList);
});
const changeInfo: UploadChangeParam<UploadFile> = {
file: file as UploadFile,

View File

@ -914,7 +914,7 @@ describe('Upload', () => {
throw new TypeError("Object doesn't support this action");
};
jest.spyOn(global, 'File').mockImplementationOnce(fileConstructor);
const spyIE = jest.spyOn(global, 'File').mockImplementationOnce(fileConstructor);
fireEvent.change(container.querySelector('input'), {
target: {
files: [{ file: 'foo.png' }],
@ -925,6 +925,7 @@ describe('Upload', () => {
await sleep();
expect(onChange.mock.calls[0][0].fileList).toHaveLength(1);
spyIE.mockRestore();
});
// https://github.com/ant-design/ant-design/issues/33819
@ -965,4 +966,58 @@ describe('Upload', () => {
const { container: wrapper2 } = render(<Upload prefixCls="custom-upload" />);
expect(wrapper2.querySelectorAll('.custom-upload-list').length).toBeGreaterThan(0);
});
// https://github.com/ant-design/ant-design/issues/36869
it('Prevent auto batch', async () => {
const mockFile1 = new File(['bamboo'], 'bamboo.png', {
type: 'image/png',
});
const mockFile2 = new File(['light'], 'light.png', {
type: 'image/png',
});
let info1;
let info2;
const onChange = jest.fn();
const { container } = render(
<Upload
customRequest={info => {
if (info.file === mockFile1) {
info1 = info;
} else {
info2 = info;
}
}}
onChange={onChange}
/>,
);
fireEvent.change(container.querySelector('input'), {
target: {
files: [mockFile1, mockFile2],
},
});
// React 18 is async now
await act(async () => {
await sleep();
});
onChange.mockReset();
// Processing
act(() => {
info1.onProgress({ percent: 10 }, mockFile1);
info2.onProgress({ percent: 20 }, mockFile2);
});
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
fileList: [
expect.objectContaining({ percent: 10 }),
expect.objectContaining({ percent: 20 }),
],
}),
);
});
});

View File

@ -16,7 +16,7 @@ Finally, if you are working in a local development environment, please refer to
Here is a simple online codesandbox demo of an Ant Design component to show the usage of Ant Design React.
<iframe
src="https://codesandbox.io/embed/antd-reproduction-template-6e93z?autoresize=1&fontsize=14&hidenavigation=1&theme=light"
src="https://codesandbox.io/embed/antd-reproduction-template-y9vgcf?autoresize=1&fontsize=14&hidenavigation=1&theme=light"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="antd reproduction template"
allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"

View File

@ -14,7 +14,7 @@ Ant Design React 致力于提供给程序员**愉悦**的开发体验。
这是一个最简单的 Ant Design 组件的在线 codesandbox 演示。
<iframe
src="https://codesandbox.io/embed/antd-reproduction-template-6e93z?autoresize=1&fontsize=14&hidenavigation=1&theme=light"
src="https://codesandbox.io/embed/antd-reproduction-template-y9vgcf?autoresize=1&fontsize=14&hidenavigation=1&theme=light"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="antd reproduction template"
allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"

View File

@ -115,6 +115,7 @@
"@ant-design/react-slick": "~0.29.1",
"@babel/runtime": "^7.18.3",
"@ctrl/tinycolor": "^3.4.0",
"@testing-library/user-event": "^14.4.2",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"dayjs": "^1.11.1",
@ -124,7 +125,7 @@
"rc-checkbox": "~2.3.0",
"rc-collapse": "~3.3.0",
"rc-dialog": "~8.9.0",
"rc-drawer": "~5.1.0-alpha.1",
"rc-drawer": "~5.1.0",
"rc-dropdown": "~4.0.0",
"rc-field-form": "~1.27.0",
"rc-image": "~5.7.0",
@ -288,7 +289,7 @@
"stylelint": "^14.9.0",
"stylelint-config-prettier": "^9.0.2",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^26.0.0",
"stylelint-config-standard": "^27.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.1.0",
"stylelint-order": "^5.0.0",
"theme-switcher": "^1.0.2",

View File

@ -58,7 +58,10 @@ async function printLog() {
type: 'list',
name: 'fromVersion',
message: '🏷 Please choose tag to compare with current branch:',
choices: tags.all.reverse().slice(0, 10),
choices: tags.all
.filter(item => !item.includes('experimental'))
.reverse()
.slice(0, 10),
},
]);
let { toVersion } = await inquirer.prompt([

View File

@ -216,6 +216,8 @@ class Demo extends React.Component {
.replace(/import\s+(?:React,\s+)?{(\s+[^}]*\s+)}\s+from\s+'react'/, `const { $1 } = React;`)
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'antd';/, 'const { $1 } = antd;')
.replace(/import\s+{(\s+[^}]*\s+)}\s+from\s+'@ant-design\/icons';/, 'const { $1 } = icons;')
.replace("import moment from 'moment';", '')
.replace("import React from 'react';", '')
.replace(/import\s+{\s+(.*)\s+}\s+from\s+'react-router';/, 'const { $1 } = ReactRouter;')
.replace(
/import\s+{\s+(.*)\s+}\s+from\s+'react-router-dom';/,
@ -250,9 +252,11 @@ class Demo extends React.Component {
const riddlePrefillConfig = {
title: `${localizedTitle} - antd@${dependencies.antd}`,
js: `${
/import React(\D*)from 'react';/.test(sourceCode) ? '' : `import React from 'react';\n`
}${
react18
? `import React from 'react';\nimport { createRoot } from 'react-dom/client';\n`
: ''
? `import { createRoot } from 'react-dom/client';\n`
: `import ReactDOM from 'react-dom';\n`
}${sourceCode.replace(/export default/, 'const ComponentDemo =')}\n\n${
react18
? 'createRoot(mountNode).render(<ComponentDemo />)'