merge feature

This commit is contained in:
zombiej 2020-02-24 13:10:29 +08:00
commit 3f8d6cd4f0
21 changed files with 316 additions and 107 deletions

View File

@ -3,6 +3,7 @@ import omit from 'omit.js';
import classNames from 'classnames';
import FieldForm, { List } from 'rc-field-form';
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import { ColProps } from '../grid/col';
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
import { FormContext } from './context';
@ -23,6 +24,7 @@ export interface FormProps extends Omit<RcFormProps, 'form'> {
wrapperCol?: ColProps;
form?: FormInstance;
size?: SizeType;
scrollToFirstError?: boolean;
}
const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props, ref) => {
@ -41,6 +43,8 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
className = '',
layout = 'horizontal',
size = contextSize,
scrollToFirstError,
onFinishFailed,
} = props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
@ -64,6 +68,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
'labelAlign',
'labelCol',
'colon',
'scrollToFirstError',
]);
const [wrapForm] = useForm(form);
@ -71,6 +76,16 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
React.useImperativeHandle(ref, () => wrapForm);
const onInternalFinishFailed = (errorInfo: ValidateErrorEntity) => {
if (onFinishFailed) {
onFinishFailed(errorInfo);
}
if (scrollToFirstError && errorInfo.errorFields.length) {
wrapForm.scrollToField(errorInfo.errorFields[0].name);
}
};
return (
<SizeContextProvider size={size}>
<FormContext.Provider
@ -83,7 +98,13 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
colon,
}}
>
<FieldForm id={name} {...formProps} form={wrapForm} className={formClassName} />
<FieldForm
id={name}
{...formProps}
onFinishFailed={onInternalFinishFailed}
form={wrapForm}
className={formClassName}
/>
</FormContext.Provider>
</SizeContextProvider>
);

View File

@ -127,7 +127,7 @@ describe('Form', () => {
const wrapper = mount(
<Form
onFinish={(v) => {
onFinish={v => {
if (typeof v.list[0] === 'object') {
/* old version led to SyntheticEvent be passed as an value here
that led to weird infinite loop somewhere and OutOfMemory crash */
@ -146,16 +146,10 @@ describe('Form', () => {
<Input />
</Form.Item>
))}
<Button
className="add"
onClick={add}
>
<Button className="add" onClick={add}>
Add
</Button>
<Button
className="remove"
onClick={() => remove(0)}
>
<Button className="remove" onClick={() => remove(0)}>
Remove
</Button>
</>
@ -258,7 +252,7 @@ describe('Form', () => {
);
};
mount(<Demo />, { attachTo: document.body });
const wrapper = mount(<Demo />, { attachTo: document.body });
expect(scrollIntoView).not.toHaveBeenCalled();
const form = callGetForm();
@ -271,6 +265,8 @@ describe('Form', () => {
block: 'start',
scrollMode: 'if-needed',
});
wrapper.unmount();
});
}
@ -297,6 +293,27 @@ describe('Form', () => {
});
});
it('scrollToFirstError', async () => {
const onFinishFailed = jest.fn();
const wrapper = mount(
<Form scrollToFirstError onFinishFailed={onFinishFailed}>
<Form.Item name="test" rules={[{ required: true }]}>
<input />
</Form.Item>
</Form>,
{ attachTo: document.body },
);
expect(scrollIntoView).not.toHaveBeenCalled();
wrapper.find('form').simulate('submit');
await delay(50);
expect(scrollIntoView).toHaveBeenCalled();
expect(onFinishFailed).toHaveBeenCalled();
wrapper.unmount();
});
it('Form.Item should support data-*、aria-* and custom attribute', () => {
const wrapper = mount(
<Form>

View File

@ -97,10 +97,6 @@ const RegistrationForm = () => {
console.log('Received values of form: ', values);
};
const onFinishFailed = ({ errorFields }) => {
form.scrollToField(errorFields[0].name);
};
const prefixSelector = (
<Form.Item name="prefix" noStyle>
<Select style={{ width: 70 }}>
@ -131,11 +127,11 @@ const RegistrationForm = () => {
form={form}
name="register"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
initialValues={{
residence: ['zhejiang', 'hangzhou', 'xihu'],
prefix: '86',
}}
scrollToFirstError
>
<Form.Item
name="email"

View File

@ -28,6 +28,7 @@ High performance Form component with data scope management. Including data colle
| labelCol | label layout, like `<Col>` component. Set `span` `offset` value like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | - |
| layout | Form layout | `horizontal` \| `vertical` \| `inline` | `horizontal` |
| name | Form name. Will be the prefix of Field `id` | string | - |
| scrollToFirstError | Auto scroll to first failed field when submit | false | - |
| size | Set field component size (antd components only) | `small` \| `middle` \| `large` | - |
| validateMessages | Validation prompt template, description [see below](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | - |

View File

@ -29,6 +29,7 @@ title: Form
| labelCol | label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | - |
| layout | 表单布局 | `horizontal` \| `vertical` \| `inline` | `horizontal` |
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - |
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | false | - |
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - |
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | - |

View File

@ -715,6 +715,100 @@ exports[`renders ./components/select/demo/custom-dropdown-menu.md correctly 1`]
</div>
`;
exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
<div
class="ant-select ant-select-multiple ant-select-show-search"
style="width:100%"
>
<div
class="ant-select-selector"
>
<span>
<span
class="ant-tag ant-tag-gold"
style="margin-right:3px"
>
gold
<span
aria-label="close"
class="anticon anticon-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</span>
</span>
<span>
<span
class="ant-tag ant-tag-cyan"
style="margin-right:3px"
>
cyan
<span
aria-label="close"
class="anticon anticon-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
class=""
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
</span>
</span>
<span
class="ant-select-selection-search"
style="width:0"
>
<input
aria-activedescendant="undefined_list_0"
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
 
</span>
</span>
</div>
</div>
`;
exports[`renders ./components/select/demo/debug.md correctly 1`] = `
<div
style="width:500px;position:relative;z-index:1;border:1px solid red;background:#FFF"

View File

@ -0,0 +1,41 @@
---
order: 24
title:
zh-CN: 自定义选择标签
en-US: Custom Tag Render
---
## zh-CN
允许自定义选择标签的样式
## en-US
Allows for custom rendering of tags
```jsx
import { Select, Tag } from 'antd';
const options = [{ value: 'gold' }, { value: 'lime' }, { value: 'green' }, { value: 'cyan' }];
function tagRender(props) {
const { label, value, closable, onClose } = props;
return (
<Tag color={value} closable={closable} onClose={onClose} style={{ marginRight: 3 }}>
{label}
</Tag>
);
}
ReactDOM.render(
<Select
mode="multiple"
tagRender={tagRender}
defaultValue={['gold', 'cyan']}
style={{ width: '100%' }}
options={options}
/>,
mountNode,
);
```

View File

@ -41,6 +41,7 @@ Select component to select value from options.
| maxTagCount | Max tag count to show | number | - | |
| maxTagTextLength | Max tag text length to show | number | - | |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode/function(omittedValues) | - | |
| tagRender | Customize tag render | (props) => ReactNode | - | |
| mode | Set mode of Select | `multiple` \| `tags` | - | |
| notFoundContent | Specify content to show when no result matches.. | string | 'Not Found' | |
| optionFilterProp | Which prop value of option will be used for filter if filterOption is true | string | value | |

View File

@ -42,6 +42,7 @@ title: Select
| maxTagCount | 最多显示多少个 tag | number | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode/function(omittedValues) | - | |
| tagRender | 自定义 tag 内容 render | (props) => ReactNode | - | |
| mode | 设置 Select 的模式为多选或标签 | `multiple` \| `tags` | - | |
| notFoundContent | 当下拉列表为空时显示的内容 | string | 'Not Found' | |
| optionFilterProp | 搜索时过滤对应的 option 属性,如设置为 children 表示对内嵌内容进行搜索。[示例](https://codesandbox.io/s/antd-reproduction-template-tk678) | string | value | |

View File

@ -62,7 +62,7 @@ ReactDOM.render(<IconSlider min={0} max={20} />, mountNode);
}
.icon-wrapper .icon-wrapper-active {
color: rgba(0, 0, 0, .45);
color: rgba(0, 0, 0, 0.45);
}
.icon-wrapper .anticon:first-child {

View File

@ -51,5 +51,5 @@ describe('Spin', () => {
const wrapper = mount(<Spin />);
expect(wrapper).toMatchSnapshot();
Spin.setDefaultIndicator(null);
})
});
});

View File

@ -49,8 +49,8 @@ class Editable extends React.Component<EditableProps, EditableState> {
if (this.textarea && this.textarea.resizableTextArea) {
const { textArea } = this.textarea.resizableTextArea;
textArea.focus();
const { length } = textArea.value;
textArea.setSelectionRange(length, length);
const { length } = textArea.value;
textArea.setSelectionRange(length, length);
}
}

View File

@ -236,7 +236,13 @@ class Upload extends React.Component<UploadProps, UploadState> {
locale: propLocale,
iconRender,
} = this.props;
const { showRemoveIcon, showPreviewIcon, showDownloadIcon, removeIcon, downloadIcon } = showUploadList as any;
const {
showRemoveIcon,
showPreviewIcon,
showDownloadIcon,
removeIcon,
downloadIcon,
} = showUploadList as any;
const { fileList } = this.state;
return (
<UploadList

View File

@ -99,19 +99,17 @@ export default class UploadList extends React.Component<UploadListProps, any> {
};
handleActionIconRender = (customIcon: React.ReactNode, callback: () => void, title?: string) => {
if(React.isValidElement(customIcon)) {
return React.cloneElement(customIcon,
{
...customIcon.props,
title,
onClick: (e: React.MouseEvent<HTMLElement>) => {
callback();
if (customIcon.props.onClick) {
customIcon.props.onClick(e);
}
},
if (React.isValidElement(customIcon)) {
return React.cloneElement(customIcon, {
...customIcon.props,
title,
onClick: (e: React.MouseEvent<HTMLElement>) => {
callback();
if (customIcon.props.onClick) {
customIcon.props.onClick(e);
}
},
);
});
}
return (
<span title={title} onClick={callback}>
@ -194,18 +192,31 @@ export default class UploadList extends React.Component<UploadListProps, any> {
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
const removeIcon = showRemoveIcon ? customRemoveIcon && (
this.handleActionIconRender(customRemoveIcon, () => this.handleClose(file), locale.removeFile)
) || (
<DeleteOutlined title={locale.removeFile} onClick={() => this.handleClose(file)} />
) : null;
const removeIcon = showRemoveIcon
? (customRemoveIcon &&
this.handleActionIconRender(
customRemoveIcon,
() => this.handleClose(file),
locale.removeFile,
)) || (
<DeleteOutlined title={locale.removeFile} onClick={() => this.handleClose(file)} />
)
: null;
const downloadIcon =
showDownloadIcon && file.status === 'done' ? customDownloadIcon && (
this.handleActionIconRender(customDownloadIcon, () => this.handleDownload(file), locale.downloadFile)
) || (
<DownloadOutlined title={locale.downloadFile} onClick={() => this.handleDownload(file)}/>
) : null;
showDownloadIcon && file.status === 'done'
? (customDownloadIcon &&
this.handleActionIconRender(
customDownloadIcon,
() => this.handleDownload(file),
locale.downloadFile,
)) || (
<DownloadOutlined
title={locale.downloadFile}
onClick={() => this.handleDownload(file)}
/>
)
: null;
const downloadOrDelete = listType !== 'picture-card' && (
<span
key="download-delete"

View File

@ -490,7 +490,11 @@ describe('Upload List', () => {
onChange={handleChange}
showUploadList={{
showRemoveIcon: true,
removeIcon: <i className="custom-delete" onClick={myClick}>RM</i>,
removeIcon: (
<i className="custom-delete" onClick={myClick}>
RM
</i>
),
}}
>
<button type="button">upload</button>
@ -609,8 +613,8 @@ describe('Upload List', () => {
items={items}
locale={{ downloadFile: '' }}
showUploadList={{ showDownloadIcon: true }}
/>,
).instance();
/>,
).instance();
expect(wrapper.handleDownload(file)).toBe(undefined);
});
@ -646,7 +650,7 @@ describe('Upload List', () => {
expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1);
});
it('extname should work correctly when url exists', (done) => {
it('extname should work correctly when url exists', done => {
const items = [{ status: 'done', uid: 'upload-list-item', url: '/example' }];
const wrapper = mount(
<UploadList

View File

@ -4,6 +4,7 @@ title:
zh-CN: 自定义交互图标
en-US: custom action icon
---
## zh-CN
使用 `showUploadList` 设置列表交互图标。

View File

@ -25,60 +25,60 @@ return (
目前支持以下语言:
| 语言 | 文件名 |
| ---------------- | ------ |
| 阿拉伯语 | ar_EG |
| 阿塞拜疆语 | az_AZ |
| 保加利亚语 | bg_BG |
| 加泰罗尼亚语 | ca_ES |
| 捷克语 | cs_CZ |
| 丹麦语 | da_DK |
| 德语 | de_DE |
| 希腊语 | el_GR |
| 英语 | en_GB |
| 英语(美式) | en_US |
| 西班牙语 | es_ES |
| 爱沙尼亚语 | et_EE |
| 波斯语 | fa_IR |
| 芬兰语 | fi_FI |
| 法语(比利时) | fr_BE |
| 语言 | 文件名 |
| ----------------- | ------ |
| 阿拉伯语 | ar_EG |
| 阿塞拜疆语 | az_AZ |
| 保加利亚语 | bg_BG |
| 加泰罗尼亚语 | ca_ES |
| 捷克语 | cs_CZ |
| 丹麦语 | da_DK |
| 德语 | de_DE |
| 希腊语 | el_GR |
| 英语 | en_GB |
| 英语(美式) | en_US |
| 西班牙语 | es_ES |
| 爱沙尼亚语 | et_EE |
| 波斯语 | fa_IR |
| 芬兰语 | fi_FI |
| 法语(比利时) | fr_BE |
| 法语(法国) | fr_FR |
| 希伯来语 | he_IL |
| 印地语 | hi_IN |
| 克罗地亚语 | hr_HR |
| 匈牙利语 | hu_HU |
| 亚美尼亚 | hy_AM |
| 印度尼西亚语 | id_ID |
| 意大利语 | it_IT |
| 冰岛语 | is_IS |
| 日语 | ja_JP |
| 库尔德语 (伊拉克) | ku_IQ |
| 卡纳达语 | kn_IN |
| 韩语/朝鲜语 | ko_KR |
| 拉脱维亚语 | lv_LV |
| 马其顿语 | mk_MK |
| 蒙古语 | mn_MN |
| 马来语 (马来西亚) | ms_MY |
| 挪威语 | nb_NO |
| 尼泊尔语 | ne_NP |
| 荷兰语(比利时) | nl_BE |
| 荷兰语 | nl_NL |
| 波兰语 | pl_PL |
| 葡萄牙语(巴西) | pt_BR |
| 葡萄牙语 | pt_PT |
| 罗马尼亚语 | ro_RO |
| 俄罗斯语 | ru_RU |
| 斯洛伐克语 | sk_SK |
| 塞尔维亚语 | sr_RS |
| 斯洛文尼亚语 | sl_SI |
| 瑞典语 | sv_SE |
| 泰米尔语 | ta_IN |
| 泰语 | th_TH |
| 土耳其语 | tr_TR |
| 乌克兰语 | uk_UA |
| 越南语 | vi_VN |
| 简体中文 | zh_CN |
| 繁体中文 | zh_TW |
| 希伯来语 | he_IL |
| 印地语 | hi_IN |
| 克罗地亚语 | hr_HR |
| 匈牙利语 | hu_HU |
| 亚美尼亚 | hy_AM |
| 印度尼西亚语 | id_ID |
| 意大利语 | it_IT |
| 冰岛语 | is_IS |
| 日语 | ja_JP |
| 库尔德语 (伊拉克) | ku_IQ |
| 卡纳达语 | kn_IN |
| 韩语/朝鲜语 | ko_KR |
| 拉脱维亚语 | lv_LV |
| 马其顿语 | mk_MK |
| 蒙古语 | mn_MN |
| 马来语 (马来西亚) | ms_MY |
| 挪威语 | nb_NO |
| 尼泊尔语 | ne_NP |
| 荷兰语(比利时) | nl_BE |
| 荷兰语 | nl_NL |
| 波兰语 | pl_PL |
| 葡萄牙语(巴西) | pt_BR |
| 葡萄牙语 | pt_PT |
| 罗马尼亚语 | ro_RO |
| 俄罗斯语 | ru_RU |
| 斯洛伐克语 | sk_SK |
| 塞尔维亚语 | sr_RS |
| 斯洛文尼亚语 | sl_SI |
| 瑞典语 | sv_SE |
| 泰米尔语 | ta_IN |
| 泰语 | th_TH |
| 土耳其语 | tr_TR |
| 乌克兰语 | uk_UA |
| 越南语 | vi_VN |
| 简体中文 | zh_CN |
| 繁体中文 | zh_TW |
具体的使用方法和新语言包贡献方式请参考 [ConfigProvider 文档](/components/config-provider)。

View File

@ -115,6 +115,7 @@ const Demo = () => (
- DatePicker rewrite
- Provide the `picker` property for selector switching.
- Range selection can now select start and end times individually.
- `onPanelChange` will also trigger when panel value changed.
- Tree, Select, TreeSelect, AutoComplete rewrite
- use virtual scrolling.
- `onBlur` no longer trigger value change.

View File

@ -115,6 +115,7 @@ const Demo = () => (
- DatePicker 重写
- 提供 `picker` 属性用于选择器切换。
- 范围选择现在可以单独选择开始和结束时间。
- `onPanelChange` 在面板值变化时也会触发。
- Tree、Select、TreeSelect、AutoComplete 重新写
- 使用虚拟滚动。
- `onBlur` 时不再修改选中值。

View File

@ -115,7 +115,7 @@
"rc-menu": "~8.0.0-alpha.7",
"rc-notification": "~4.0.0-rc.1",
"rc-pagination": "~1.21.0",
"rc-picker": "~0.0.1-rc.6",
"rc-picker": "~1.0.0",
"rc-progress": "~2.5.0",
"rc-rate": "~2.5.1",
"rc-resize-observer": "^0.1.0",
@ -272,4 +272,4 @@
}
],
"title": "Ant Design"
}
}

View File

@ -23,12 +23,12 @@ export default class ColorPaletteTool extends Component {
});
};
handleChangeBackgroundColor = (e) => {
handleChangeBackgroundColor = e => {
const value = e.target ? e.target.value : e;
this.setState({
backgroundColor: value,
});
}
};
renderColorValidation() {
const { primaryColorInstance } = this.state;
@ -45,7 +45,11 @@ export default class ColorPaletteTool extends Component {
).toFixed(2)}`;
}
}
return <span className="color-palette-picker-validation color-palette-picker-validation-dark">{text.trim()}</span>;
return (
<span className="color-palette-picker-validation color-palette-picker-validation-dark">
{text.trim()}
</span>
);
}
render() {
@ -64,7 +68,11 @@ export default class ColorPaletteTool extends Component {
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<Row>
<Col span={18}>
<ColorPicker type="chrome" color={primaryColor} onChange={this.handleChangeColor} />
<ColorPicker
type="chrome"
color={primaryColor}
onChange={this.handleChangeColor}
/>
</Col>
<Col span={6}>
<span className="color-palette-pick-hex">{primaryColor}</span>
@ -79,7 +87,11 @@ export default class ColorPaletteTool extends Component {
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<Row>
<Col span={18}>
<ColorPicker type="chrome" color={backgroundColor} onChange={this.handleChangeBackgroundColor} />
<ColorPicker
type="chrome"
color={backgroundColor}
onChange={this.handleChangeBackgroundColor}
/>
</Col>
<Col span={6}>
<span className="color-palette-pick-hex">{backgroundColor}</span>