Merge pull request #24371 from ant-design/master

chore: merge master into feature
This commit is contained in:
偏右 2020-05-21 23:41:37 +08:00 committed by GitHub
commit ae8603838a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 697 additions and 678 deletions

View File

@ -22,10 +22,10 @@ references:
ignore: gh-pages
- dist:
requires:
- setup
- setup
- compile:
requires:
- setup
- setup
- lint:
requires:
- setup
@ -166,7 +166,7 @@ workflows:
<<: *workflow
triggers:
- schedule:
cron: "0 0 * * *"
cron: '0 0 * * *'
filters:
branches:
only:

View File

@ -1,19 +1,24 @@
name: Lighthouse
on: push
name: Lighthouse Check
on: [pull_request]
jobs:
lighthouse:
lighthouse-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Audit URLs using Lighthouse
uses: treosh/lighthouse-ci-action@v2
- uses: actions/checkout@master
- run: mkdir /tmp/artifacts
- name: Run Lighthouse
uses: foo-software/lighthouse-check-action@master
id: lighthouseCheck
with:
urls: |
https://ant.design
https://ant.design/docs/react/introduce-cn
https://ant.design/components/button-cn
- name: Save results
uses: actions/upload-artifact@v1
accessToken: ${{ secrets.GITHUB_TOKEN }}
outputDirectory: /tmp/artifacts
emulatedFormFactor: desktop
timeout: 1200
prCommentEnabled: false
urls: 'https://preview-${{ github.event.pull_request.number }}-ant-design.surge.sh,https://preview-${{ github.event.pull_request.number }}-ant-design.surge.sh/components/button'
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: lighthouse-results
path: '.lighthouseci' # This will save the Lighthouse results as .json files
name: Lighthouse reports
path: /tmp/artifacts

31
.github/workflows/ui-ci.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: UI-TEST
on:
issue_comment:
types: [created]
jobs:
ui:
runs-on: ubuntu-latest
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/ui')
steps:
- name: checkout
uses: actions/checkout@master
- name: install
run: npm install
- name: dist
run: npm run dist
- name: test
run: npm run test:image
- name: VERCEL Now Deployment
uses: amondnet/now-deployment@v2.0.3
with:
zeit-token: ${{ secrets.VERCEL_TOKEN }}
now-project-id: ${{ secrets.VERCEL_PROJECT_ID}}
now-org-id: ${{ secrets.VERCEL_ORG_ID}}
working-directory: ./jest-stare
if: failure()

4
.gitignore vendored
View File

@ -58,3 +58,7 @@ site/theme/template/Resources/**/*.jsx
site/theme/template/NotFound.jsx
scripts/previewEditor/index.html
components/version/version.tsx
# Image snapshot diff
__diff_output__/
/jest-stare

24
.jest.image.js Normal file
View File

@ -0,0 +1,24 @@
const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
// jest config for image snapshots
module.exports = {
setupFiles: ['./tests/setup.js'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
moduleNameMapper,
transform: {
'\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},
testRegex: 'image\\.test\\.js$',
testEnvironment: 'node',
transformIgnorePatterns,
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: {
'ts-jest': {
tsConfigFile: './tsconfig.test.json',
},
},
reporters: ['default', 'jest-stare'],
};

View File

@ -26,7 +26,7 @@ module.exports = {
'^react-dnd-test-backend$': 'react-dnd-test-backend/dist/cjs',
'^react-dnd-test-utils$': 'react-dnd-test-utils/dist/cjs',
},
testPathIgnorePatterns: ['/node_modules/', 'dekko', 'node'],
testPathIgnorePatterns: ['/node_modules/', 'dekko', 'node', 'image.test.js'],
transform: {
'\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
import ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';
@ -12,7 +11,7 @@ import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import Animate from 'rc-animate';
import classNames from 'classnames';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import getDataOrAriaProps from '../_util/getDataOrAriaProps';
import ErrorBoundary from './ErrorBoundary';
import { replaceElement } from '../_util/reactNode';
@ -48,11 +47,6 @@ export interface AlertProps {
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
export interface AlertState {
closing: boolean;
closed: boolean;
}
const iconMapFilled = {
success: CheckCircleFilled,
info: InfoCircleFilled,
@ -67,66 +61,67 @@ const iconMapOutlined = {
warning: ExclamationCircleOutlined,
};
export default class Alert extends React.Component<AlertProps, AlertState> {
static ErrorBoundary = ErrorBoundary;
interface AlertInterface extends React.FC<AlertProps> {
ErrorBoundary: typeof ErrorBoundary;
}
state = {
closing: false,
closed: false,
};
const Alert: AlertInterface = ({
description,
prefixCls: customizePrefixCls,
message,
banner,
className = '',
style,
onMouseEnter,
onMouseLeave,
onClick,
showIcon,
closable,
closeText,
...props
}) => {
const [closing, setClosing] = React.useState(false);
const [closed, setClosed] = React.useState(false);
handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
const ref = React.useRef<HTMLElement>();
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('alert', customizePrefixCls);
const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const dom = ReactDOM.findDOMNode(this) as HTMLElement;
const dom = ref.current as HTMLElement;
dom.style.height = `${dom.offsetHeight}px`;
// Magic code
// 重复一次后才能正确设置 height
dom.style.height = `${dom.offsetHeight}px`;
this.setState({
closing: true,
});
this.props.onClose?.(e);
setClosing(true);
props.onClose?.(e);
};
animationEnd = () => {
this.setState({
closing: false,
closed: true,
});
this.props.afterClose?.();
const animationEnd = () => {
setClosing(false);
setClosed(true);
props.afterClose?.();
};
getShowIcon() {
const { banner, showIcon } = this.props;
// banner 模式默认有 Icon
return banner && showIcon === undefined ? true : showIcon;
}
getType() {
const { banner, type } = this.props;
const getType = () => {
const { type } = props;
if (type !== undefined) {
return type;
}
// banner 模式默认为警告
return banner ? 'warning' : 'info';
}
};
getClosable() {
const { closable, closeText } = this.props;
// closeable when closeText is assigned
return closeText ? true : closable;
}
// closeable when closeText is assigned
const isClosable = closeText ? true : closable;
const type = getType();
getIconType() {
const { description } = this.props;
const renderIconNode = () => {
const { icon } = props;
// use outline icon in alert with description
return (description ? iconMapOutlined : iconMapFilled)[this.getType()] || null;
}
renderIconNode({ prefixCls }: { prefixCls: string }) {
const { icon } = this.props;
const iconType = this.getIconType();
const iconType = (description ? iconMapOutlined : iconMapFilled)[type] || null;
if (icon) {
return replaceElement(icon, <span className={`${prefixCls}-icon`}>{icon}</span>, () => ({
className: classNames(`${prefixCls}-icon`, {
@ -135,14 +130,13 @@ export default class Alert extends React.Component<AlertProps, AlertState> {
}));
}
return React.createElement(iconType, { className: `${prefixCls}-icon` });
}
};
renderCloseIcon({ prefixCls }: { prefixCls: string }) {
const { closeText } = this.props;
return this.getClosable() ? (
const renderCloseIcon = () => {
return isClosable ? (
<button
type="button"
onClick={this.handleClose}
onClick={handleClose}
className={`${prefixCls}-close-icon`}
tabIndex={0}
>
@ -153,74 +147,53 @@ export default class Alert extends React.Component<AlertProps, AlertState> {
)}
</button>
) : null;
}
renderAlert = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
description,
prefixCls: customizePrefixCls,
message,
banner,
className = '',
style,
onMouseEnter,
onMouseLeave,
onClick,
} = this.props;
const { closing, closed } = this.state;
const prefixCls = getPrefixCls('alert', customizePrefixCls);
const isShowIcon = this.getShowIcon();
const type = this.getType();
const closable = this.getClosable();
const alertCls = classNames(
prefixCls,
`${prefixCls}-${type}`,
{
[`${prefixCls}-closing`]: closing,
[`${prefixCls}-with-description`]: !!description,
[`${prefixCls}-no-icon`]: !isShowIcon,
[`${prefixCls}-banner`]: !!banner,
[`${prefixCls}-closable`]: closable,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const closeIcon = this.renderCloseIcon({ prefixCls });
const dataOrAriaProps = getDataOrAriaProps(this.props);
const iconNode = this.renderIconNode({ prefixCls });
return closed ? null : (
<Animate
component=""
showProp="data-show"
transitionName={`${prefixCls}-slide-up`}
onEnd={this.animationEnd}
>
<div
data-show={!closing}
className={alertCls}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick}
{...dataOrAriaProps}
>
{isShowIcon ? iconNode : null}
<span className={`${prefixCls}-message`}>{message}</span>
<span className={`${prefixCls}-description`}>{description}</span>
{closeIcon}
</div>
</Animate>
);
};
render() {
return <ConfigConsumer>{this.renderAlert}</ConfigConsumer>;
}
}
// banner 模式默认有 Icon
const isShowIcon = banner && showIcon === undefined ? true : showIcon;
const alertCls = classNames(
prefixCls,
`${prefixCls}-${type}`,
{
[`${prefixCls}-closing`]: closing,
[`${prefixCls}-with-description`]: !!description,
[`${prefixCls}-no-icon`]: !isShowIcon,
[`${prefixCls}-banner`]: !!banner,
[`${prefixCls}-closable`]: isClosable,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
const dataOrAriaProps = getDataOrAriaProps(props);
return closed ? null : (
<Animate
component=""
showProp="data-show"
transitionName={`${prefixCls}-slide-up`}
onEnd={animationEnd}
>
<div
ref={ref}
data-show={!closing}
className={alertCls}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick}
{...dataOrAriaProps}
>
{isShowIcon ? renderIconNode() : null}
<span className={`${prefixCls}-message`}>{message}</span>
<span className={`${prefixCls}-description`}>{description}</span>
{renderCloseIcon()}
</div>
</Animate>
);
};
Alert.ErrorBoundary = ErrorBoundary;
export default Alert;

View File

@ -16,6 +16,7 @@ export interface AvatarProps {
src?: string;
/** Srcset of image avatar */
srcSet?: string;
draggable?: boolean;
/** icon to be used in avatar */
icon?: React.ReactNode;
style?: React.CSSProperties;
@ -106,6 +107,7 @@ export default class Avatar extends React.Component<AvatarProps, AvatarState> {
icon,
className,
alt,
draggable,
...others
} = this.props;
@ -142,7 +144,15 @@ export default class Avatar extends React.Component<AvatarProps, AvatarState> {
let { children } = this.props;
if (src && isImgExist) {
children = <img src={src} srcSet={srcSet} onError={this.handleImgLoadError} alt={alt} />;
children = (
<img
src={src}
draggable={draggable}
srcSet={srcSet}
onError={this.handleImgLoadError}
alt={alt}
/>
);
} else if (icon) {
children = icon;
} else {

View File

@ -22,6 +22,12 @@
line-height: @line-height-base;
.btn;
.btn-default;
// Fix loading button animation
// https://github.com/ant-design/ant-design/issues/24323
> span {
display: inline-block;
}
&-primary {
.btn-primary;

View File

@ -16,10 +16,7 @@ describe('Calendar', () => {
rtlTest(Calendar, true);
function openSelect(wrapper, className) {
wrapper
.find(className)
.find('.ant-select-selector')
.simulate('mousedown');
wrapper.find(className).find('.ant-select-selector').simulate('mousedown');
}
function findSelectItem(wrapper) {
@ -27,18 +24,13 @@ describe('Calendar', () => {
}
function clickSelectItem(wrapper, index = 0) {
findSelectItem(wrapper)
.at(index)
.simulate('click');
findSelectItem(wrapper).at(index).simulate('click');
}
it('Calendar should be selectable', () => {
const onSelect = jest.fn();
const wrapper = mount(<Calendar onSelect={onSelect} />);
wrapper
.find('.ant-picker-cell')
.at(0)
.simulate('click');
wrapper.find('.ant-picker-cell').at(0).simulate('click');
expect(onSelect).toHaveBeenCalledWith(expect.anything());
const value = onSelect.mock.calls[0][0];
expect(Moment.isMoment(value)).toBe(true);
@ -50,14 +42,8 @@ describe('Calendar', () => {
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');
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);
});
@ -67,10 +53,7 @@ describe('Calendar', () => {
const wrapper = mount(
<Calendar onSelect={onSelect} validRange={validRange} defaultValue={Moment('2018-02-02')} />,
);
wrapper
.find('[title="2018-02-20"]')
.at(0)
.simulate('click');
wrapper.find('[title="2018-02-20"]').at(0).simulate('click');
const elem = wrapper.find('[title="2018-02-20"]').hasClass('ant-picker-cell-disabled');
expect(elem).toEqual(true);
expect(onSelect.mock.calls.length).toBe(0);
@ -87,32 +70,13 @@ describe('Calendar', () => {
mode="year"
/>,
);
expect(
wrapper
.find('[title="2018-01"]')
.at(0)
.hasClass('ant-picker-cell-disabled'),
).toBe(true);
expect(
wrapper
.find('[title="2018-02"]')
.at(0)
.hasClass('ant-picker-cell-disabled'),
).toBe(false);
expect(
wrapper
.find('[title="2018-06"]')
.at(0)
.hasClass('ant-picker-cell-disabled'),
).toBe(true);
wrapper
.find('[title="2018-01"]')
.at(0)
.simulate('click');
wrapper
.find('[title="2018-03"]')
.at(0)
.simulate('click');
expect(wrapper.find('[title="2018-01"]').at(0).hasClass('ant-picker-cell-disabled')).toBe(true);
expect(wrapper.find('[title="2018-02"]').at(0).hasClass('ant-picker-cell-disabled')).toBe(
false,
);
expect(wrapper.find('[title="2018-06"]').at(0).hasClass('ant-picker-cell-disabled')).toBe(true);
wrapper.find('[title="2018-01"]').at(0).simulate('click');
wrapper.find('[title="2018-03"]').at(0).simulate('click');
expect(onSelect.mock.calls.length).toBe(1);
});
@ -155,7 +119,7 @@ describe('Calendar', () => {
});
it('Calendar should support locale', () => {
MockDate.set(Moment('2018-10-19'));
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} />);
@ -168,10 +132,7 @@ describe('Calendar', () => {
const date = new Moment('1990-09-03');
const wrapper = mount(<Calendar onPanelChange={onPanelChange} value={date} />);
wrapper
.find('.ant-picker-cell')
.at(0)
.simulate('click');
wrapper.find('.ant-picker-cell').at(0).simulate('click');
expect(onPanelChange).toHaveBeenCalled();
expect(onPanelChange.mock.calls[0][0].month()).toEqual(date.month() - 1);
@ -242,10 +203,7 @@ describe('Calendar', () => {
/>,
);
openSelect(wrapper, '.ant-picker-calendar-year-select');
wrapper
.find('.ant-select-item-option')
.last()
.simulate('click');
wrapper.find('.ant-select-item-option').last().simulate('click');
expect(onValueChange).toHaveBeenCalledWith(value.year('2019').month('2'));
});
@ -283,10 +241,7 @@ describe('Calendar', () => {
type="date"
/>,
);
wrapper
.find('input[type="radio"]')
.at(1)
.simulate('change');
wrapper.find('input[type="radio"]').at(1).simulate('change');
expect(onTypeChange).toHaveBeenCalledWith('year');
});
@ -324,9 +279,7 @@ describe('Calendar', () => {
openSelect(wrapperWithYear, '.ant-select');
wrapperWithYear.update();
findSelectItem(wrapperWithYear)
.last()
.simulate('click');
findSelectItem(wrapperWithYear).last().simulate('click');
expect(onYearChange).toHaveBeenCalled();
@ -371,9 +324,7 @@ describe('Calendar', () => {
openSelect(wrapperWithMonth, '.ant-select');
wrapperWithMonth.update();
findSelectItem(wrapperWithMonth)
.last()
.simulate('click');
findSelectItem(wrapperWithMonth).last().simulate('click');
expect(onMonthChange).toHaveBeenCalled();
@ -391,10 +342,7 @@ describe('Calendar', () => {
<Calendar fullscreen={false} headerRender={headerRenderWithTypeChange} />,
);
wrapperWithTypeChange
.find('.ant-radio-button-input')
.last()
.simulate('change');
wrapperWithTypeChange.find('.ant-radio-button-input').last().simulate('change');
expect(onTypeChange).toHaveBeenCalled();
});
@ -402,23 +350,13 @@ describe('Calendar', () => {
const wrapper = mount(
<Calendar dateFullCellRender={() => <div className="light">Bamboo</div>} />,
);
expect(
wrapper
.find('.light')
.first()
.text(),
).toEqual('Bamboo');
expect(wrapper.find('.light').first().text()).toEqual('Bamboo');
});
it('monthFullCellRender', () => {
const wrapper = mount(
<Calendar mode="year" monthFullCellRender={() => <div className="bamboo">Light</div>} />,
);
expect(
wrapper
.find('.bamboo')
.first()
.text(),
).toEqual('Light');
expect(wrapper.find('.bamboo').first().text()).toEqual('Light');
});
});

View File

@ -11,7 +11,7 @@ describe('DatePicker', () => {
focusTest(DatePicker, { refFocus: true });
beforeEach(() => {
MockDate.set(moment('2016-11-22'));
MockDate.set(moment('2016-11-22').valueOf());
});
afterEach(() => {

View File

@ -194,7 +194,7 @@ Provide linkage between forms. If a sub form with `name` prop update, it will au
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean |
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/blob/ece40bd9143f48caf4b99503425ecb16b0ad8249/src/types.ts#L10)]) => void |
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void |
| setFieldsValue | Set fields value | (values) => void |
| submit | Submit the form. It's same as click `submit` button | () => void |

View File

@ -195,7 +195,7 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/blob/ece40bd9143f48caf4b99503425ecb16b0ad8249/src/types.ts#L10)]) => void |
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void |
| setFieldsValue | 设置表单的值 | (values) => void |
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |

View File

@ -0,0 +1,37 @@
import React from 'react';
import { Col, Row } from '..';
import imageTest from '../../../tests/shared/imageTest';
describe('Grid image', () => {
imageTest(
<>
<Row>
<Col>col</Col>
</Row>
<Row>
<Col>col</Col>
<Col>col</Col>
<Col>col</Col>
<Col>col</Col>
</Row>
<Row>
<Col span={24}>col</Col>
</Row>
<Row>
<Col span={12}>col-12</Col>
<Col span={12}>col-12</Col>
</Row>
<Row>
<Col span={8}>col-8</Col>
<Col span={8}>col-8</Col>
<Col span={8}>col-8</Col>
</Row>
<Row>
<Col span={6}>col-6</Col>
<Col span={6}>col-6</Col>
<Col span={6}>col-6</Col>
<Col span={6}>col-6</Col>
</Row>
</>,
);
});

View File

@ -47,5 +47,6 @@
position: absolute;
top: 0;
right: 0;
z-index: 1;
margin: 8px 8px 0 0;
}

View File

@ -107708,6 +107708,9 @@ exports[`Locale Provider should display the text as id 1`] = `
>
<thead>
<tr>
<th>
Mg
</th>
<th>
Sn
</th>
@ -107726,13 +107729,20 @@ exports[`Locale Provider should display the text as id 1`] = `
<th>
Sb
</th>
<th>
Mg
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="ant-picker-cell"
title="2017-08-27"
>
<div
class="ant-picker-cell-inner"
>
27
</div>
</td>
<td
class="ant-picker-cell"
title="2017-08-28"
@ -107793,6 +107803,8 @@ exports[`Locale Provider should display the text as id 1`] = `
2
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-03"
@ -107803,8 +107815,6 @@ exports[`Locale Provider should display the text as id 1`] = `
3
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-04"
@ -107865,6 +107875,8 @@ exports[`Locale Provider should display the text as id 1`] = `
9
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-10"
@ -107875,8 +107887,6 @@ exports[`Locale Provider should display the text as id 1`] = `
10
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-11"
@ -107937,6 +107947,8 @@ exports[`Locale Provider should display the text as id 1`] = `
16
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-17"
@ -107947,8 +107959,6 @@ exports[`Locale Provider should display the text as id 1`] = `
17
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view ant-picker-cell-today"
title="2017-09-18"
@ -108009,6 +108019,8 @@ exports[`Locale Provider should display the text as id 1`] = `
23
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-24"
@ -108019,8 +108031,6 @@ exports[`Locale Provider should display the text as id 1`] = `
24
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-25"
@ -108081,6 +108091,8 @@ exports[`Locale Provider should display the text as id 1`] = `
30
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-01"
@ -108091,8 +108103,6 @@ exports[`Locale Provider should display the text as id 1`] = `
1
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-02"
@ -108153,16 +108163,6 @@ exports[`Locale Provider should display the text as id 1`] = `
7
</div>
</td>
<td
class="ant-picker-cell"
title="2017-10-08"
>
<div
class="ant-picker-cell-inner"
>
8
</div>
</td>
</tr>
</tbody>
</table>
@ -109767,6 +109767,9 @@ exports[`Locale Provider should display the text as id 1`] = `
>
<thead>
<tr>
<th>
Mg
</th>
<th>
Sn
</th>
@ -109785,13 +109788,20 @@ exports[`Locale Provider should display the text as id 1`] = `
<th>
Sb
</th>
<th>
Mg
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="ant-picker-cell"
title="2017-08-27"
>
<div
class="ant-picker-cell-inner"
>
27
</div>
</td>
<td
class="ant-picker-cell"
title="2017-08-28"
@ -109852,6 +109862,8 @@ exports[`Locale Provider should display the text as id 1`] = `
2
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-03"
@ -109862,8 +109874,6 @@ exports[`Locale Provider should display the text as id 1`] = `
3
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-04"
@ -109924,6 +109934,8 @@ exports[`Locale Provider should display the text as id 1`] = `
9
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-10"
@ -109934,8 +109946,6 @@ exports[`Locale Provider should display the text as id 1`] = `
10
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-11"
@ -109996,6 +110006,8 @@ exports[`Locale Provider should display the text as id 1`] = `
16
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-17"
@ -110006,8 +110018,6 @@ exports[`Locale Provider should display the text as id 1`] = `
17
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view ant-picker-cell-today"
title="2017-09-18"
@ -110068,6 +110078,8 @@ exports[`Locale Provider should display the text as id 1`] = `
23
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-24"
@ -110078,8 +110090,6 @@ exports[`Locale Provider should display the text as id 1`] = `
24
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-25"
@ -110140,6 +110150,8 @@ exports[`Locale Provider should display the text as id 1`] = `
30
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-01"
@ -110150,8 +110162,6 @@ exports[`Locale Provider should display the text as id 1`] = `
1
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-02"
@ -110212,16 +110222,6 @@ exports[`Locale Provider should display the text as id 1`] = `
7
</div>
</td>
<td
class="ant-picker-cell"
title="2017-10-08"
>
<div
class="ant-picker-cell-inner"
>
8
</div>
</td>
</tr>
</tbody>
</table>
@ -110303,6 +110303,9 @@ exports[`Locale Provider should display the text as id 1`] = `
>
<thead>
<tr>
<th>
Mg
</th>
<th>
Sn
</th>
@ -110321,73 +110324,10 @@ exports[`Locale Provider should display the text as id 1`] = `
<th>
Sb
</th>
<th>
Mg
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="ant-picker-cell"
title="2017-09-25"
>
<div
class="ant-picker-cell-inner"
>
25
</div>
</td>
<td
class="ant-picker-cell"
title="2017-09-26"
>
<div
class="ant-picker-cell-inner"
>
26
</div>
</td>
<td
class="ant-picker-cell"
title="2017-09-27"
>
<div
class="ant-picker-cell-inner"
>
27
</div>
</td>
<td
class="ant-picker-cell"
title="2017-09-28"
>
<div
class="ant-picker-cell-inner"
>
28
</div>
</td>
<td
class="ant-picker-cell"
title="2017-09-29"
>
<div
class="ant-picker-cell-inner"
>
29
</div>
</td>
<td
class="ant-picker-cell"
title="2017-09-30"
>
<div
class="ant-picker-cell-inner"
>
30
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-01"
@ -110398,8 +110338,6 @@ exports[`Locale Provider should display the text as id 1`] = `
1
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-02"
@ -110460,6 +110398,8 @@ exports[`Locale Provider should display the text as id 1`] = `
7
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-08"
@ -110470,8 +110410,6 @@ exports[`Locale Provider should display the text as id 1`] = `
8
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-09"
@ -110532,6 +110470,8 @@ exports[`Locale Provider should display the text as id 1`] = `
14
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-15"
@ -110542,8 +110482,6 @@ exports[`Locale Provider should display the text as id 1`] = `
15
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-16"
@ -110604,6 +110542,8 @@ exports[`Locale Provider should display the text as id 1`] = `
21
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-22"
@ -110614,8 +110554,6 @@ exports[`Locale Provider should display the text as id 1`] = `
22
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-23"
@ -110676,6 +110614,8 @@ exports[`Locale Provider should display the text as id 1`] = `
28
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-29"
@ -110686,8 +110626,6 @@ exports[`Locale Provider should display the text as id 1`] = `
29
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-10-30"
@ -110748,6 +110686,8 @@ exports[`Locale Provider should display the text as id 1`] = `
4
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-11-05"
@ -110758,6 +110698,66 @@ exports[`Locale Provider should display the text as id 1`] = `
5
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-06"
>
<div
class="ant-picker-cell-inner"
>
6
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-07"
>
<div
class="ant-picker-cell-inner"
>
7
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-08"
>
<div
class="ant-picker-cell-inner"
>
8
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-09"
>
<div
class="ant-picker-cell-inner"
>
9
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-10"
>
<div
class="ant-picker-cell-inner"
>
10
</div>
</td>
<td
class="ant-picker-cell"
title="2017-11-11"
>
<div
class="ant-picker-cell-inner"
>
11
</div>
</td>
</tr>
</tbody>
</table>
@ -111373,6 +111373,9 @@ exports[`Locale Provider should display the text as id 1`] = `
>
<thead>
<tr>
<th>
Mg
</th>
<th>
Sn
</th>
@ -111391,13 +111394,27 @@ exports[`Locale Provider should display the text as id 1`] = `
<th>
Sb
</th>
<th>
Mg
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="ant-picker-cell"
title="2017-08-27"
>
<div
class="ant-picker-cell-inner ant-picker-calendar-date"
>
<div
class="ant-picker-calendar-date-value"
>
27
</div>
<div
class="ant-picker-calendar-date-content"
/>
</div>
</td>
<td
class="ant-picker-cell"
title="2017-08-28"
@ -111500,6 +111517,8 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-03"
@ -111517,8 +111536,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-04"
@ -111621,6 +111638,8 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-10"
@ -111638,8 +111657,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-11"
@ -111742,6 +111759,8 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-17"
@ -111759,8 +111778,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view ant-picker-cell-today ant-picker-cell-selected"
title="2017-09-18"
@ -111863,6 +111880,8 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-24"
@ -111880,8 +111899,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2017-09-25"
@ -111984,6 +112001,8 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-01"
@ -112001,8 +112020,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2017-10-02"
@ -112105,23 +112122,6 @@ exports[`Locale Provider should display the text as id 1`] = `
/>
</div>
</td>
<td
class="ant-picker-cell"
title="2017-10-08"
>
<div
class="ant-picker-cell-inner ant-picker-calendar-date"
>
<div
class="ant-picker-calendar-date-value"
>
08
</div>
<div
class="ant-picker-calendar-date-content"
/>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -174,7 +174,7 @@ describe('Locale Provider', () => {
));
beforeAll(() => {
MockDate.set(moment('2017-09-18T03:30:07.795'));
MockDate.set(moment('2017-09-18T03:30:07.795').valueOf());
});
afterAll(() => {

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Button from '../button';
import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/button';
@ -11,40 +10,34 @@ export interface ActionButtonProps {
buttonProps?: ButtonProps;
}
export interface ActionButtonState {
loading: ButtonProps['loading'];
}
const ActionButton: React.FC<ActionButtonProps> = props => {
const clickedRef = React.useRef<boolean>(false);
const ref = React.useRef<any>();
const [loading, setLoading] = React.useState<ButtonProps['loading']>(false);
export default class ActionButton extends React.Component<ActionButtonProps, ActionButtonState> {
timeoutId: number;
clicked: boolean;
state = {
loading: false,
};
componentDidMount() {
if (this.props.autoFocus) {
const $this = ReactDOM.findDOMNode(this) as HTMLInputElement;
this.timeoutId = setTimeout(() => $this.focus());
React.useEffect(() => {
let timeoutId: number;
if (props.autoFocus) {
const $this = ref.current as HTMLInputElement;
timeoutId = setTimeout(() => $this.focus());
}
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
componentWillUnmount() {
clearTimeout(this.timeoutId);
}
handlePromiseOnOk(returnValueOfOnOk?: PromiseLike<any>) {
const { closeModal } = this.props;
const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => {
const { closeModal } = props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) {
return;
}
this.setState({ loading: true });
setLoading(true);
returnValueOfOnOk.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
// this.setState({ loading: false });
// setState({ loading: false });
closeModal(...args);
},
(e: Error) => {
@ -52,18 +45,18 @@ export default class ActionButton extends React.Component<ActionButtonProps, Act
// eslint-disable-next-line no-console
console.error(e);
// See: https://github.com/ant-design/ant-design/issues/6183
this.setState({ loading: false });
this.clicked = false;
setLoading(false);
clickedRef.current = false;
},
);
}
};
onClick = () => {
const { actionFn, closeModal } = this.props;
if (this.clicked) {
const onClick = () => {
const { actionFn, closeModal } = props;
if (clickedRef.current) {
return;
}
this.clicked = true;
clickedRef.current = true;
if (!actionFn) {
closeModal();
return;
@ -72,7 +65,7 @@ export default class ActionButton extends React.Component<ActionButtonProps, Act
if (actionFn.length) {
returnValueOfOnOk = actionFn(closeModal);
// https://github.com/ant-design/ant-design/issues/23358
this.clicked = false;
clickedRef.current = false;
} else {
returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) {
@ -80,21 +73,21 @@ export default class ActionButton extends React.Component<ActionButtonProps, Act
return;
}
}
this.handlePromiseOnOk(returnValueOfOnOk);
handlePromiseOnOk(returnValueOfOnOk);
};
render() {
const { type, children, buttonProps } = this.props;
const { loading } = this.state;
return (
<Button
{...convertLegacyProps(type)}
onClick={this.onClick}
loading={loading}
{...buttonProps}
>
{children}
</Button>
);
}
}
const { type, children, buttonProps } = props;
return (
<Button
{...convertLegacyProps(type)}
onClick={onClick}
loading={loading}
{...buttonProps}
ref={ref}
>
{children}
</Button>
);
};
export default ActionButton;

View File

@ -9,7 +9,7 @@ import { getConfirmLocale } from './locale';
import Button from '../button';
import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/button';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
let mousePosition: { x: number; y: number } | null;
export const destroyFns: Array<() => void> = [];
@ -119,46 +119,41 @@ export interface ModalLocale {
justOkText: string;
}
export default class Modal extends React.Component<ModalProps, {}> {
static destroyAll: () => void;
interface ModalInterface extends React.FC<ModalProps> {
useModal: typeof useModal;
}
static useModal = useModal;
const Modal: ModalInterface = props => {
const { getPopupContainer: getContextPopupContainer, getPrefixCls, direction } = React.useContext(
ConfigContext,
);
static defaultProps = {
width: 520,
transitionName: 'zoom',
maskTransitionName: 'fade',
confirmLoading: false,
visible: false,
okType: 'primary' as LegacyButtonType,
};
handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onCancel } = this.props;
const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onCancel } = props;
if (onCancel) {
onCancel(e);
}
};
handleOk = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onOk } = this.props;
const handleOk = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onOk } = props;
if (onOk) {
onOk(e);
}
};
renderFooter = (locale: ModalLocale) => {
const { okText, okType, cancelText, confirmLoading } = this.props;
const renderFooter = (locale: ModalLocale) => {
const { okText, okType, cancelText, confirmLoading } = props;
return (
<>
<Button onClick={this.handleCancel} {...this.props.cancelButtonProps}>
<Button onClick={handleCancel} {...props.cancelButtonProps}>
{cancelText || locale.cancelText}
</Button>
<Button
{...convertLegacyProps(okType)}
loading={confirmLoading}
onClick={this.handleOk}
{...this.props.okButtonProps}
onClick={handleOk}
{...props.okButtonProps}
>
{okText || locale.okText}
</Button>
@ -166,54 +161,58 @@ export default class Modal extends React.Component<ModalProps, {}> {
);
};
renderModal = ({
getPopupContainer: getContextPopupContainer,
getPrefixCls,
direction,
}: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
footer,
visible,
wrapClassName,
centered,
getContainer,
closeIcon,
...restProps
} = this.props;
const {
prefixCls: customizePrefixCls,
footer,
visible,
wrapClassName,
centered,
getContainer,
closeIcon,
...restProps
} = props;
const prefixCls = getPrefixCls('modal', customizePrefixCls);
const defaultFooter = (
<LocaleReceiver componentName="Modal" defaultLocale={getConfirmLocale()}>
{this.renderFooter}
</LocaleReceiver>
);
const prefixCls = getPrefixCls('modal', customizePrefixCls);
const defaultFooter = (
<LocaleReceiver componentName="Modal" defaultLocale={getConfirmLocale()}>
{renderFooter}
</LocaleReceiver>
);
const closeIconToRender = (
<span className={`${prefixCls}-close-x`}>
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
</span>
);
const wrapClassNameExtended = classNames(wrapClassName, {
[`${prefixCls}-centered`]: !!centered,
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
return (
<Dialog
{...restProps}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={this.handleCancel}
closeIcon={closeIconToRender}
/>
);
};
const closeIconToRender = (
<span className={`${prefixCls}-close-x`}>
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
</span>
);
render() {
return <ConfigConsumer>{this.renderModal}</ConfigConsumer>;
}
}
const wrapClassNameExtended = classNames(wrapClassName, {
[`${prefixCls}-centered`]: !!centered,
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
return (
<Dialog
{...restProps}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={handleCancel}
closeIcon={closeIconToRender}
/>
);
};
Modal.useModal = useModal;
Modal.defaultProps = {
width: 520,
transitionName: 'zoom',
maskTransitionName: 'fade',
confirmLoading: false,
visible: false,
okType: 'primary' as LegacyButtonType,
};
export default Modal;

View File

@ -52,15 +52,15 @@ describe('Modal', () => {
it('onCancel should be called', () => {
const onCancel = jest.fn();
const wrapper = mount(<Modal onCancel={onCancel} />).instance();
wrapper.handleCancel();
const wrapper = mount(<Modal visible onCancel={onCancel} />);
wrapper.find('.ant-btn').first().simulate('click');
expect(onCancel).toHaveBeenCalled();
});
it('onOk should be called', () => {
const onOk = jest.fn();
const wrapper = mount(<Modal onOk={onOk} />).instance();
wrapper.handleOk();
const wrapper = mount(<Modal visible onOk={onOk} />);
wrapper.find('.ant-btn').last().simulate('click');
expect(onOk).toHaveBeenCalled();
});

View File

@ -30,8 +30,8 @@ When requiring users to interact with the application, but without jumping to a
| maskStyle | Style for modal's mask element. | object | {} |
| okText | Text of the OK button | string\|ReactNode | `OK` |
| okType | Button `type` of the OK button | string | `primary` |
| okButtonProps | The ok button props | [ButtonProps](/components/button) | - |
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button) | - |
| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - |
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - |
| style | Style of floating layer, typically used at least for adjusting the position. | CSSProperties | - |
| title | The modal dialog's title | string\|ReactNode | - |
| visible | Whether the modal dialog is visible or not | boolean | false |
@ -57,26 +57,26 @@ There are five ways to display the information based on the content's nature:
The items listed above are all functions, expecting a settings object as parameter. The properties of the object are follows:
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| autoFocusButton | Specify which button to autofocus | null\| `ok` \| `cancel` | `ok` |
| cancelText | Text of the Cancel button with Modal.confirm | string | `Cancel` |
| centered | Centered Modal | Boolean | `false` |
| className | className of container | string | - |
| content | Content | string\|ReactNode | - |
| icon | custom icon (`Added in 3.12.0`) | ReactNode | `<QuestionCircle />` |
| keyboard | Whether support press esc to close | Boolean | true |
| mask | Whether show mask or not. | Boolean | true |
| maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | Boolean | `false` |
| okText | Text of the OK button | string | `OK` |
| okType | Button `type` of the OK button | string | `primary` |
| okButtonProps | The ok button props | [ButtonProps](/components/button) | - |
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button) | - |
| title | Title | string\|ReactNode | - |
| width | Width of the modal dialog | string\|number | 416 |
| zIndex | The `z-index` of the Modal | Number | 1000 |
| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - |
| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| autoFocusButton | Specify which button to autofocus | null\| `ok` \| `cancel` | `ok` | |
| cancelText | Text of the Cancel button with Modal.confirm | string | `Cancel` | |
| centered | Centered Modal | Boolean | `false` | |
| className | className of container | string | - | |
| content | Content | string\|ReactNode | - | |
| icon | custom icon | ReactNode | [<QuestionCircle /\>](/components/icon/) | 3.12.0 |
| keyboard | Whether support press esc to close | Boolean | true | |
| mask | Whether show mask or not. | Boolean | true | |
| maskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | Boolean | `false` | |
| okText | Text of the OK button | string | `OK` | |
| okType | Button `type` of the OK button | string | `primary` | |
| okButtonProps | The ok button props | [ButtonProps](/components/button/#API) | - | |
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | |
| title | Title | string\|ReactNode | - | |
| width | Width of the modal dialog | string\|number | 416 | |
| zIndex | The `z-index` of the Modal | Number | 1000 | |
| onCancel | Specify a function that will be called when the user clicks the Cancel button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | |
| onOk | Specify a function that will be called when the user clicks the OK button. The parameter of this function is a function whose execution should include closing the dialog. You can also just return a promise and when the promise is resolved, the modal dialog will also be closed | function(close) | - | |
All the `Modal.method`s will return a reference, and then we can update and close the modal dialog by the reference.

View File

@ -15,7 +15,7 @@ function modalWarn(props: ModalFuncProps) {
return confirm(withWarn(props));
}
type Modal = typeof OriginModal & ModalStaticFunctions;
type Modal = typeof OriginModal & ModalStaticFunctions & { destroyAll: () => void };
const Modal = OriginModal as Modal;
Modal.info = function infoFn(props: ModalFuncProps) {

View File

@ -34,8 +34,8 @@ title: Modal
| maskStyle | 遮罩样式 | object | {} |
| okText | 确认按钮文字 | string\|ReactNode | 确定 |
| okType | 确认按钮类型 | string | primary |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button) | - |
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button) | - |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - |
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - |
| style | 可用于设置浮层的样式,调整浮层位置等 | CSSProperties | - |
| title | 标题 | string\|ReactNode | - |
| visible | 对话框是否可见 | boolean | - |
@ -61,24 +61,24 @@ title: Modal
以上均为一个函数,参数为 object具体属性如下
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| autoFocusButton | 指定自动获得焦点的按钮 | null\| `ok` \| `cancel` | `ok` |
| cancelText | 设置 Modal.confirm 取消按钮文字 | string | 取消 |
| centered | 垂直居中展示 Modal | Boolean | `false` |
| className | 容器类名 | string | - |
| content | 内容 | string\|ReactNode | - |
| icon | 自定义图标3.12.0 新增) | ReactNode | `<QuestionCircle />` |
| maskClosable | 点击蒙层是否允许关闭 | Boolean | `false` |
| okText | 确认按钮文字 | string | 确定 |
| okType | 确认按钮类型 | string | primary |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button) | - |
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button) | - |
| title | 标题 | string\|ReactNode | - |
| width | 宽度 | string\|number | 416 |
| zIndex | 设置 Modal 的 `z-index` | Number | 1000 |
| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - |
| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| autoFocusButton | 指定自动获得焦点的按钮 | null\| `ok` \| `cancel` | `ok` | |
| cancelText | 设置 Modal.confirm 取消按钮文字 | string | 取消 | |
| centered | 垂直居中展示 Modal | Boolean | `false` | |
| className | 容器类名 | string | - | |
| content | 内容 | string\|ReactNode | - | |
| icon | 自定义图标 | ReactNode | [<QuestionCircle /\>](/components/icon/) | 3.12.0 |
| maskClosable | 点击蒙层是否允许关闭 | Boolean | `false` | |
| okText | 确认按钮文字 | string | 确定 | |
| okType | 确认按钮类型 | string | primary | |
| okButtonProps | ok 按钮 props | [ButtonProps](/components/button/#API) | - | |
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | |
| title | 标题 | string\|ReactNode | - | |
| width | 宽度 | string\|number | 416 | |
| zIndex | 设置 Modal 的 `z-index` | Number | 1000 | |
| onCancel | 取消回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | |
| onOk | 点击确定回调,参数为关闭函数,返回 promise 时 resolve 后自动关闭 | function(close) | - | |
以上函数调用后,会返回一个引用,可以通过该引用更新和关闭弹窗。

View File

@ -50,4 +50,23 @@ describe('notification.hooks', () => {
expect(document.querySelectorAll('.my-test-notification-notice').length).toBe(1);
expect(document.querySelector('.hook-test-result').innerHTML).toEqual('bamboo');
});
it('should be same hook', () => {
let count = 0;
const Demo = () => {
const [, forceUpdate] = React.useState({});
const [api] = notification.useNotification();
React.useEffect(() => {
count += 1;
expect(count).toEqual(1);
forceUpdate();
}, [api]);
return null;
};
mount(<Demo />);
});
});

View File

@ -46,19 +46,20 @@ export default function createUseNotification(
}
// Fill functions
const hookAPI: any = {
open: notify,
};
const hookApiRef = React.useRef<any>({});
hookApiRef.current.open = notify;
['success', 'info', 'warning', 'error'].forEach(type => {
hookAPI[type] = (args: ArgsProps) =>
hookAPI.open({
hookApiRef.current[type] = (args: ArgsProps) =>
hookApiRef.current.open({
...args,
type,
});
});
return [
hookAPI,
hookApiRef.current,
<ConfigConsumer key="holder">
{(context: ConfigConsumerProps) => {
({ getPrefixCls } = context);

View File

@ -14,7 +14,7 @@ describe('Statistic', () => {
rtlTest(Statistic);
beforeAll(() => {
MockDate.set(moment('2018-11-28 00:00:00'));
MockDate.set(moment('2018-11-28 00:00:00').valueOf());
});
afterAll(() => {
@ -102,7 +102,7 @@ describe('Statistic', () => {
const wrapper = mount(<Statistic.Countdown value={now} onFinish={onFinish} />);
wrapper.update();
MockDate.set(moment('2019-11-28 00:00:00'));
MockDate.set(moment('2019-11-28 00:00:00').valueOf());
jest.runAllTimers();
expect(onFinish).toHaveBeenCalled();

View File

@ -299,7 +299,6 @@
// Grid system
@grid-columns: 24;
@grid-gutter-width: 0;
// Layout
@layout-body-background: #f0f2f5;

View File

@ -797,4 +797,28 @@ describe('Table.rowSelection', () => {
.simulate('change', { target: { checked: true } });
expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]);
});
it('do not cache selected keys', () => {
const onChange = jest.fn();
const wrapper = mount(
<Table
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
rowSelection={{ onChange }}
rowKey="name"
/>,
);
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
wrapper
.find('tbody input')
.first()
.simulate('change', { target: { checked: true } });
expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
});
});

View File

@ -117,12 +117,21 @@ export default function useSelection<RecordType>(
const setSelectedKeys = React.useCallback(
(keys: Key[]) => {
setInnerSelectedKeys(keys);
const availableKeys: Key[] = [];
const records: RecordType[] = [];
const records = keys.map(key => getRecordByKey(key));
keys.forEach(key => {
const record = getRecordByKey(key);
if (record !== undefined) {
availableKeys.push(key);
records.push(record);
}
});
setInnerSelectedKeys(availableKeys);
if (onSelectionChange) {
onSelectionChange(keys, records);
onSelectionChange(availableKeys, records);
}
},
[setInnerSelectedKeys, getRecordByKey, onSelectionChange],

View File

@ -72,7 +72,7 @@ class Upload extends React.Component<UploadProps, UploadState> {
this.clearProgressTimer();
}
saveUpload = (node: typeof RcUpload) => {
saveUpload = (node: any) => {
this.upload = node;
};

View File

@ -50,6 +50,10 @@ toc: false
- https://gw.alipayobjects.com/zos/basement_prod/7b9ed3f2-6f05-4ddb-bac3-d55feb71e0ac.svg
- 在 Figma 使用 Ant Design 进行设计
- https://www.antforfigma.com
- Figma 开源组件包
- https://i.imgur.com/XoLEWxA.png
- 代码级精确度的免费开源 Figma 完全组件库
- https://www.figma.com/community/file/831698976089873405
- 全新 Chart 组件包
- https://gw.alipayobjects.com/zos/basement_prod/a9dc586a-fe0a-4c7d-ab4f-f5ed779b963d.svg
- 桌面组件 Chart 模板包

View File

@ -1,111 +0,0 @@
[build]
publish = "_site"
command = "npm run site"
[[redirects]]
from = "/docs/resource/download"
to = "/docs/spec/download"
status = 301
force = false
[[redirects]]
from = "/docs/resource/download-cn"
to = "/docs/spec/download-cn"
status = 301
force = false
[[redirects]]
from = "/docs/resource/reference"
to = "/docs/spec/reference"
status = 301
force = false
[[redirects]]
from = "/docs/resource/reference-cn"
to = "/docs/spec/reference-cn"
status = 301
force = false
[[redirects]]
from = "/docs/spec/feature"
to = "/docs/spec/values"
status = 301
force = false
[[redirects]]
from = "/docs/spec/feature-cn"
to = "/docs/spec/values-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/advanced-search"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/advanced-search-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/complex-table"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/complex-table-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/form"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/form-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/list"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/list-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/navigation"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/navigation-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/table"
to = "/docs/spec/overview"
status = 301
force = false
[[redirects]]
from = "/docs/pattern/table-cn"
to = "/docs/spec/overview-cn"
status = 301
force = false

View File

@ -62,7 +62,7 @@
"deploy": "bisheng gh-pages --push-only --dotfiles",
"deploy:china-mirror": "git checkout gh-pages && git pull origin gh-pages && git push git@gitee.com:ant-design/ant-design.git gh-pages",
"dist": "antd-tools run dist",
"lint": "npm run lint:tsc && npm run lint:script && npm run lint:demo && npm run lint:style && npm run lint:deps && npm run lint:md",
"lint": "npm run tsc && npm run lint:script && npm run lint:demo && npm run lint:style && npm run lint:deps && npm run lint:md",
"lint-fix": "npm run lint-fix:script && npm run lint-fix:demo && npm run lint-fix:style",
"lint-fix:demo": "eslint-tinker ./components/*/demo/*.md",
"lint-fix:script": "npm run lint:script -- --fix",
@ -72,7 +72,6 @@
"lint:md": "remark . -f -q",
"lint:script": "eslint . --ext '.js,.jsx,.ts,.tsx'",
"lint:style": "stylelint '{site,components}/**/*.less' --syntax less",
"lint:tsc": "npm run tsc -- --noEmit",
"pre-publish": "npm run check-commit && npm run test-all",
"prettier": "prettier -c --write '**/*'",
"pretty-quick": "pretty-quick",
@ -87,8 +86,9 @@
"test:update": "jest --config .jest.js --no-cache --update-snapshot",
"test-all": "./scripts/test-all.sh",
"test-node": "jest --config .jest.node.js --no-cache",
"tsc": "tsc",
"tsc": "tsc --noEmit",
"site:test": "jest --config .jest.site.js --cache=false",
"test:image": "npm install puppeteer@2.1.1 --no-save && jest --config .jest.image.js --no-cache",
"version": "node ./scripts/generate-version"
},
"husky": {
@ -110,7 +110,7 @@
"classnames": "~2.2.6",
"copy-to-clipboard": "^3.2.0",
"lodash": "^4.17.13",
"moment": "~2.25.3",
"moment": "^2.25.3",
"omit.js": "^1.0.2",
"prop-types": "^15.7.2",
"raf": "^3.4.1",
@ -159,8 +159,10 @@
"@types/enzyme": "^3.10.5",
"@types/gtag.js": "^0.0.3",
"@types/jest": "^25.1.0",
"@types/jest-image-snapshot": "^3.1.0",
"@types/lodash": "^4.14.139",
"@types/prop-types": "^15.7.1",
"@types/puppeteer": "^3.0.0",
"@types/raf": "^3.4.0",
"@types/react": "^16.9.21",
"@types/react-color": "^3.0.1",
@ -213,6 +215,8 @@
"inquirer": "^7.1.0",
"intersection-observer": "^0.10.0",
"jest": "^26.0.0",
"jest-image-snapshot": "^4.0.0",
"jest-stare": "^2.0.1",
"jquery": "^3.4.1",
"jsdom": "^16.0.0",
"jsonml.js": "^0.1.0",
@ -220,7 +224,7 @@
"logrocket": "^1.0.0",
"logrocket-react": "^4.0.0",
"lz-string": "^1.4.4",
"mockdate": "^2.0.2",
"mockdate": "^3.0.0",
"node-fetch": "^2.6.0",
"open": "^7.0.3",
"preact": "^10.0.0",
@ -228,7 +232,7 @@
"prettier": "^2.0.1",
"pretty-quick": "^2.0.0",
"querystring": "^0.2.0",
"rc-footer": "^0.6.0",
"rc-footer": "^0.6.3",
"rc-queue-anim": "^1.6.12",
"rc-scroll-anim": "^2.5.8",
"rc-tween-one": "^2.4.1",

12
tests/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Amazing Antd</title>
<link rel="stylesheet" href="antd.css" />
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -53,7 +53,7 @@ export default function demoTest(component: string, options: Options = {}) {
testMethod = test.skip;
}
testMethod(`renders ${file} correctly`, () => {
MockDate.set(moment('2016-11-22').toDate());
MockDate.set(moment('2016-11-22').valueOf());
const demo = require(`../.${file}`).default; // eslint-disable-line global-require, import/no-dynamic-require
const wrapper = render(demo);

38
tests/shared/imageTest.ts Normal file
View File

@ -0,0 +1,38 @@
import React from 'react';
// Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386
// eslint-disable-next-line import/no-unresolved
import puppeteer, { Browser, Page } from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
import ReactDOMServer from 'react-dom/server';
expect.extend({ toMatchImageSnapshot });
// eslint-disable-next-line jest/no-export
export default function imageTest(component: React.ReactElement) {
describe(`Image test`, () => {
let browser: Browser;
let page: Page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
await page.goto(`file://${process.cwd()}/tests/index.html`);
await page.addStyleTag({ path: `${process.cwd()}/dist/antd.css` });
});
afterAll(() => {
browser.close();
});
it('component image screenshot should correct', async () => {
const html = ReactDOMServer.renderToString(component);
await page.evaluate(innerHTML => {
document.querySelector('#root')!.innerHTML = innerHTML;
}, html);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
});
}

View File

@ -9,7 +9,7 @@ export default function rtlTest(Component: React.ComponentType, mockDate?: boole
describe(`rtl render`, () => {
it(`component should be rendered correctly in RTL direction`, () => {
if (mockDate) {
MockDate.set(Moment('2000-09-28').toDate());
MockDate.set(Moment('2000-09-28').valueOf());
}
const wrapper = mount(
<ConfigProvider direction="rtl">

View File

@ -1,8 +1,7 @@
import moment from 'moment';
import MockDate from 'mockdate';
export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
MockDate.set(moment(dateString).toDate());
MockDate.set(dateString);
}
export function resetMockDate() {