mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-29 13:47:02 +08:00
Merge pull request #28619 from ant-design/feature
This commit is contained in:
commit
c47f81ed58
@ -793,6 +793,34 @@ Array [
|
||||
class="ant-image-img"
|
||||
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>,
|
||||
<span
|
||||
|
@ -6,7 +6,14 @@ import { PickerLocale } from '../generatePicker';
|
||||
const locale: PickerLocale = {
|
||||
lang: {
|
||||
placeholder: 'Odaberite datum',
|
||||
yearPlaceholder: 'Odaberite godinu',
|
||||
quarterPlaceholder: 'Odaberite četvrtinu',
|
||||
monthPlaceholder: 'Odaberite mjesec',
|
||||
weekPlaceholder: 'Odaberite tjedan',
|
||||
rangePlaceholder: ['Početni datum', 'Završni datum'],
|
||||
rangeYearPlaceholder: ['Početna godina', 'Završna godina'],
|
||||
rangeMonthPlaceholder: ['Početni mjesec', 'Završni mjesec'],
|
||||
rangeWeekPlaceholder: ['Početni tjedan', 'Završni tjedan'],
|
||||
...CalendarLocale,
|
||||
},
|
||||
timePickerLocale: {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { DescriptionsItemProps } from './Item';
|
||||
import Cell from './Cell';
|
||||
import { DescriptionsContext, DescriptionsContextProps } from '.';
|
||||
|
||||
interface CellConfig {
|
||||
component: string | [string, string];
|
||||
@ -12,7 +13,14 @@ interface CellConfig {
|
||||
function renderCells(
|
||||
items: React.ReactElement<DescriptionsItemProps>[],
|
||||
{ colon, prefixCls, bordered }: RowProps,
|
||||
{ component, type, showLabel, showContent }: CellConfig,
|
||||
{
|
||||
component,
|
||||
type,
|
||||
showLabel,
|
||||
showContent,
|
||||
labelStyle: rootLabelStyle,
|
||||
contentStyle: rootContentStyle,
|
||||
}: CellConfig & DescriptionsContextProps,
|
||||
) {
|
||||
return items.map(
|
||||
(
|
||||
@ -37,8 +45,8 @@ function renderCells(
|
||||
key={`${type}-${key || index}`}
|
||||
className={className}
|
||||
style={style}
|
||||
labelStyle={labelStyle}
|
||||
contentStyle={contentStyle}
|
||||
labelStyle={{ ...rootLabelStyle, ...labelStyle }}
|
||||
contentStyle={{ ...rootContentStyle, ...contentStyle }}
|
||||
span={span}
|
||||
colon={colon}
|
||||
component={component}
|
||||
@ -54,7 +62,7 @@ function renderCells(
|
||||
<Cell
|
||||
key={`label-${key || index}`}
|
||||
className={className}
|
||||
style={{ ...style, ...labelStyle }}
|
||||
style={{ ...rootLabelStyle, ...style, ...labelStyle }}
|
||||
span={1}
|
||||
colon={colon}
|
||||
component={component[0]}
|
||||
@ -65,7 +73,7 @@ function renderCells(
|
||||
<Cell
|
||||
key={`content-${key || index}`}
|
||||
className={className}
|
||||
style={{ ...style, ...contentStyle }}
|
||||
style={{ ...rootContentStyle, ...style, ...contentStyle }}
|
||||
span={span * 2 - 1}
|
||||
component={component[1]}
|
||||
itemPrefixCls={itemPrefixCls}
|
||||
@ -87,18 +95,26 @@ export interface RowProps {
|
||||
}
|
||||
|
||||
const Row: React.FC<RowProps> = props => {
|
||||
const descContext = React.useContext(DescriptionsContext);
|
||||
|
||||
const { prefixCls, vertical, row, index, bordered } = props;
|
||||
if (vertical) {
|
||||
return (
|
||||
<>
|
||||
<tr key={`label-${index}`} className={`${prefixCls}-row`}>
|
||||
{renderCells(row, props, { component: 'th', type: 'label', showLabel: true })}
|
||||
{renderCells(row, props, {
|
||||
component: 'th',
|
||||
type: 'label',
|
||||
showLabel: true,
|
||||
...descContext,
|
||||
})}
|
||||
</tr>
|
||||
<tr key={`content-${index}`} className={`${prefixCls}-row`}>
|
||||
{renderCells(row, props, {
|
||||
component: 'td',
|
||||
type: 'content',
|
||||
showContent: true,
|
||||
...descContext,
|
||||
})}
|
||||
</tr>
|
||||
</>
|
||||
@ -112,6 +128,7 @@ const Row: React.FC<RowProps> = props => {
|
||||
type: 'item',
|
||||
showLabel: true,
|
||||
showContent: true,
|
||||
...descContext,
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
|
@ -960,6 +960,165 @@ Array [
|
||||
</table>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
class="ant-divider ant-divider-horizontal"
|
||||
role="separator"
|
||||
/>,
|
||||
<div
|
||||
class="ant-descriptions"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
Root style
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr
|
||||
class="ant-descriptions-row"
|
||||
>
|
||||
<td
|
||||
class="ant-descriptions-item"
|
||||
colspan="1"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-item-container"
|
||||
>
|
||||
<span
|
||||
class="ant-descriptions-item-label"
|
||||
style="background:red"
|
||||
>
|
||||
Product
|
||||
</span>
|
||||
<span
|
||||
class="ant-descriptions-item-content"
|
||||
style="background:green"
|
||||
>
|
||||
Cloud Database
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-descriptions-item"
|
||||
colspan="1"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-item-container"
|
||||
>
|
||||
<span
|
||||
class="ant-descriptions-item-label"
|
||||
style="background:red"
|
||||
>
|
||||
Billing Mode
|
||||
</span>
|
||||
<span
|
||||
class="ant-descriptions-item-content"
|
||||
style="background:green"
|
||||
>
|
||||
Prepaid
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="ant-descriptions-item"
|
||||
colspan="1"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-item-container"
|
||||
>
|
||||
<span
|
||||
class="ant-descriptions-item-label"
|
||||
style="background:red;color:orange"
|
||||
>
|
||||
Automatic Renewal
|
||||
</span>
|
||||
<span
|
||||
class="ant-descriptions-item-content"
|
||||
style="background:green;color:blue"
|
||||
>
|
||||
YES
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
class="ant-descriptions ant-descriptions-bordered"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-header"
|
||||
>
|
||||
<div
|
||||
class="ant-descriptions-title"
|
||||
>
|
||||
Root style
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-descriptions-view"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr
|
||||
class="ant-descriptions-row"
|
||||
>
|
||||
<th
|
||||
class="ant-descriptions-item-label"
|
||||
colspan="1"
|
||||
style="background:red"
|
||||
>
|
||||
Product
|
||||
</th>
|
||||
<td
|
||||
class="ant-descriptions-item-content"
|
||||
colspan="1"
|
||||
style="background:green"
|
||||
>
|
||||
Cloud Database
|
||||
</td>
|
||||
<th
|
||||
class="ant-descriptions-item-label"
|
||||
colspan="1"
|
||||
style="background:red"
|
||||
>
|
||||
Billing Mode
|
||||
</th>
|
||||
<td
|
||||
class="ant-descriptions-item-content"
|
||||
colspan="1"
|
||||
style="background:green"
|
||||
>
|
||||
Prepaid
|
||||
</td>
|
||||
<th
|
||||
class="ant-descriptions-item-label"
|
||||
colspan="1"
|
||||
style="background:red;color:orange"
|
||||
>
|
||||
Automatic Renewal
|
||||
</th>
|
||||
<td
|
||||
class="ant-descriptions-item-content"
|
||||
colspan="1"
|
||||
style="background:green;color:blue"
|
||||
>
|
||||
YES
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -15,28 +15,53 @@ debug: true
|
||||
Customize label & wrapper style
|
||||
|
||||
```tsx
|
||||
import { Descriptions } from 'antd';
|
||||
import { Descriptions, Divider } from 'antd';
|
||||
|
||||
const labelStyle: React.CSSProperties = { background: 'red' };
|
||||
const contentStyle: React.CSSProperties = { background: 'green' };
|
||||
|
||||
function renderCelledDesc(bordered?: boolean) {
|
||||
return (
|
||||
<Descriptions title="User Info" bordered={bordered}>
|
||||
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
|
||||
Cloud Database
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
|
||||
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRootDesc(bordered?: boolean) {
|
||||
return (
|
||||
<Descriptions
|
||||
title="Root style"
|
||||
labelStyle={labelStyle}
|
||||
contentStyle={contentStyle}
|
||||
bordered={bordered}
|
||||
>
|
||||
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
|
||||
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label="Automatic Renewal"
|
||||
labelStyle={{ color: 'orange' }}
|
||||
contentStyle={{ color: 'blue' }}
|
||||
>
|
||||
YES
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<Descriptions title="User Info">
|
||||
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
|
||||
Cloud Database
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
|
||||
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{renderCelledDesc()}
|
||||
{renderCelledDesc(true)}
|
||||
|
||||
<Descriptions title="User Info" bordered>
|
||||
<Descriptions.Item label="Product" labelStyle={labelStyle} contentStyle={contentStyle}>
|
||||
Cloud Database
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
|
||||
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
|
||||
{renderRootDesc()}
|
||||
{renderRootDesc(true)}
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
|
@ -21,7 +21,9 @@ Commonly displayed on the details page.
|
||||
| bordered | Whether to display the border | boolean | false | |
|
||||
| colon | Change default props `colon` value of Descriptions.Item | boolean | true | |
|
||||
| column | The number of `DescriptionItems` in a row,could be a number or a object like `{ xs: 8, sm: 16, md: 24}`,(Only set `bordered={true}` to take effect) | number | 3 | |
|
||||
| contentStyle | Customize label style | CSSProperties | - | 4.10.0 |
|
||||
| extra | The action area of the description list, placed at the top-right | ReactNode | - | 4.5.0 |
|
||||
| labelStyle | Customize label style | CSSProperties | - | 4.10.0 |
|
||||
| layout | Define description layout | `horizontal` \| `vertical` | `horizontal` | |
|
||||
| size | Set the size of the list. Can be set to `middle`,`small`, or not filled | `default` \| `middle` \| `small` | - | |
|
||||
| title | The title of the description list, placed at the top | ReactNode | - | |
|
||||
|
@ -13,6 +13,13 @@ import Row from './Row';
|
||||
import DescriptionsItem from './Item';
|
||||
import { cloneElement } from '../_util/reactNode';
|
||||
|
||||
export interface DescriptionsContextProps {
|
||||
labelStyle?: React.CSSProperties;
|
||||
contentStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const DescriptionsContext = React.createContext<DescriptionsContextProps>({});
|
||||
|
||||
const DEFAULT_COLUMN_MAP: Record<Breakpoint, number> = {
|
||||
xxl: 3,
|
||||
xl: 3,
|
||||
@ -104,6 +111,8 @@ export interface DescriptionsProps {
|
||||
column?: number | Partial<Record<Breakpoint, number>>;
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
colon?: boolean;
|
||||
labelStyle?: React.CSSProperties;
|
||||
contentStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
function Descriptions({
|
||||
@ -118,6 +127,8 @@ function Descriptions({
|
||||
className,
|
||||
style,
|
||||
size,
|
||||
labelStyle,
|
||||
contentStyle,
|
||||
}: DescriptionsProps) {
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('descriptions', customizePrefixCls);
|
||||
@ -142,43 +153,45 @@ function Descriptions({
|
||||
const rows = getRows(children, mergedColumn);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-${size}`]: size && size !== 'default',
|
||||
[`${prefixCls}-bordered`]: !!bordered,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{(title || extra) && (
|
||||
<div className={`${prefixCls}-header`}>
|
||||
{title && <div className={`${prefixCls}-title`}>{title}</div>}
|
||||
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
|
||||
</div>
|
||||
)}
|
||||
<DescriptionsContext.Provider value={{ labelStyle, contentStyle }}>
|
||||
<div
|
||||
className={classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-${size}`]: size && size !== 'default',
|
||||
[`${prefixCls}-bordered`]: !!bordered,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{(title || extra) && (
|
||||
<div className={`${prefixCls}-header`}>
|
||||
{title && <div className={`${prefixCls}-title`}>{title}</div>}
|
||||
{extra && <div className={`${prefixCls}-extra`}>{extra}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${prefixCls}-view`}>
|
||||
<table>
|
||||
<tbody>
|
||||
{rows.map((row, index) => (
|
||||
<Row
|
||||
key={index}
|
||||
index={index}
|
||||
colon={colon}
|
||||
prefixCls={prefixCls}
|
||||
vertical={layout === 'vertical'}
|
||||
bordered={bordered}
|
||||
row={row}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={`${prefixCls}-view`}>
|
||||
<table>
|
||||
<tbody>
|
||||
{rows.map((row, index) => (
|
||||
<Row
|
||||
key={index}
|
||||
index={index}
|
||||
colon={colon}
|
||||
prefixCls={prefixCls}
|
||||
vertical={layout === 'vertical'}
|
||||
bordered={bordered}
|
||||
row={row}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DescriptionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/MjtG9_FOI/Descriptions.svg
|
||||
| bordered | 是否展示边框 | boolean | false | |
|
||||
| colon | 配置 `Descriptions.Item` 的 `colon` 的默认值 | boolean | true | |
|
||||
| column | 一行的 `DescriptionItems` 数量,可以写成像素值或支持响应式的对象写法 `{ xs: 8, sm: 16, md: 24}` | number | 3 | |
|
||||
| contentStyle | 自定义内容样式 | CSSProperties | - | 4.10.0 |
|
||||
| extra | 描述列表的操作区域,显示在右上方 | ReactNode | - | 4.5.0 |
|
||||
| labelStyle | 自定义标签样式 | CSSProperties | - | 4.10.0 |
|
||||
| layout | 描述布局 | `horizontal` \| `vertical` | `horizontal` | |
|
||||
| size | 设置列表的大小。可以设置为 `middle` 、`small`, 或不填(只有设置 `bordered={true}` 生效) | `default` \| `middle` \| `small` | - | |
|
||||
| title | 描述列表的标题,显示在最顶部 | ReactNode | - | |
|
||||
|
@ -4,6 +4,7 @@ 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 { Options } from 'scroll-into-view-if-needed';
|
||||
import { ColProps } from '../grid/col';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { FormContext, FormContextProps } from './context';
|
||||
@ -24,7 +25,7 @@ export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form
|
||||
wrapperCol?: ColProps;
|
||||
form?: FormInstance<Values>;
|
||||
size?: SizeType;
|
||||
scrollToFirstError?: boolean;
|
||||
scrollToFirstError?: Options | boolean;
|
||||
requiredMark?: RequiredMark;
|
||||
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
|
||||
hideRequiredMark?: boolean;
|
||||
@ -106,8 +107,13 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
onFinishFailed(errorInfo);
|
||||
}
|
||||
|
||||
let defaultScrollToFirstError: Options = { block: 'nearest' };
|
||||
|
||||
if (scrollToFirstError && errorInfo.errorFields.length) {
|
||||
wrapForm.scrollToField(errorInfo.errorFields[0].name);
|
||||
if (typeof scrollToFirstError === 'object') {
|
||||
defaultScrollToFirstError = scrollToFirstError;
|
||||
}
|
||||
wrapForm.scrollToField(errorInfo.errorFields[0].name, defaultScrollToFirstError);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -5167,33 +5167,42 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-select-selector"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-search"
|
||||
style="width:0"
|
||||
<div
|
||||
class="ant-select-selection-overflow"
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="validate_other_select-multiple_list_0"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="validate_other_select-multiple_list"
|
||||
aria-haspopup="listbox"
|
||||
aria-owns="validate_other_select-multiple_list"
|
||||
autocomplete="off"
|
||||
class="ant-select-selection-search-input"
|
||||
id="validate_other_select-multiple"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity:0"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
<div
|
||||
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
|
||||
style="opacity:1"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="ant-select-selection-search"
|
||||
style="width:0"
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="validate_other_select-multiple_list_0"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="validate_other_select-multiple_list"
|
||||
aria-haspopup="listbox"
|
||||
aria-owns="validate_other_select-multiple_list"
|
||||
autocomplete="off"
|
||||
class="ant-select-selection-search-input"
|
||||
id="validate_other_select-multiple"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity:0"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="ant-select-selection-placeholder"
|
||||
>
|
||||
|
@ -227,7 +227,7 @@ describe('Form', () => {
|
||||
const onFinishFailed = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<Form scrollToFirstError onFinishFailed={onFinishFailed}>
|
||||
<Form scrollToFirstError={{ block: 'center' }} onFinishFailed={onFinishFailed}>
|
||||
<Form.Item name="test" rules={[{ required: true }]}>
|
||||
<input />
|
||||
</Form.Item>
|
||||
@ -238,7 +238,11 @@ describe('Form', () => {
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
wrapper.find('form').simulate('submit');
|
||||
await sleep(50);
|
||||
expect(scrollIntoView).toHaveBeenCalled();
|
||||
const inputNode = document.getElementById('test');
|
||||
expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
|
||||
block: 'center',
|
||||
scrollMode: 'if-needed',
|
||||
});
|
||||
expect(onFinishFailed).toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
|
@ -30,7 +30,7 @@ High performance Form component with data scope management. Including data colle
|
||||
| name | Form name. Will be the prefix of Field `id` | string | - | |
|
||||
| preserve | Keep field value even when field removed | boolean | true | 4.4.0 |
|
||||
| requiredMark | Required mark style. Can use required mark or optional mark. You can not config to single Form.Item since this is a Form level config | boolean \| `optional` | true | 4.6.0 |
|
||||
| scrollToFirstError | Auto scroll to first failed field when submit | boolean | false | |
|
||||
| scrollToFirstError | Auto scroll to first failed field when submit | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | 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) | - | |
|
||||
| validateTrigger | Config field validate trigger | string \| string\[] | `onChange` | 4.3.0 |
|
||||
|
@ -31,7 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/ORmcdeaoO/Form.svg
|
||||
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - | |
|
||||
| preserve | 当字段被删除时保留字段值 | boolean | true | 4.4.0 |
|
||||
| requiredMark | 必选样式,可以切换为必选或者可选展示样式。此为 Form 配置,Form.Item 无法单独配置 | boolean \| `optional` | true | 4.6.0 |
|
||||
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean | false | |
|
||||
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | false | |
|
||||
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - | |
|
||||
| validateMessages | 验证提示模板,说明[见下](#validateMessages) | [ValidateMessages](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) | - | |
|
||||
| validateTrigger | 统一设置字段校验规则 | string \| string\[] | `onChange` | 4.3.0 |
|
||||
|
@ -9,6 +9,34 @@ exports[`renders ./components/image/demo/basic.md correctly 1`] = `
|
||||
class="ant-image-img"
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -22,6 +50,34 @@ exports[`renders ./components/image/demo/fallback.md correctly 1`] = `
|
||||
src="error"
|
||||
style="height:200px"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -55,6 +111,34 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -82,6 +166,34 @@ Array [
|
||||
class="ant-image-img"
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
class="ant-image"
|
||||
@ -91,6 +203,34 @@ Array [
|
||||
class="ant-image-img"
|
||||
src="https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
@ -7,5 +7,33 @@ exports[`Image rtl render component should be rendered correctly in RTL directio
|
||||
<img
|
||||
class="ant-image-img"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -1,5 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import EyeOutlined from '@ant-design/icons/EyeOutlined';
|
||||
import RcImage, { ImageProps } from 'rc-image';
|
||||
import defaultLocale from '../locale/en_US';
|
||||
import PreviewGroup from './PreviewGroup';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
|
||||
@ -7,11 +10,34 @@ export interface CompositionImage<P> extends React.FC<P> {
|
||||
PreviewGroup: typeof PreviewGroup;
|
||||
}
|
||||
|
||||
const Image: CompositionImage<ImageProps> = ({ prefixCls: customizePrefixCls, ...otherProps }) => {
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const Image: CompositionImage<ImageProps> = ({
|
||||
prefixCls: customizePrefixCls,
|
||||
preview,
|
||||
...otherProps
|
||||
}) => {
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('image', customizePrefixCls);
|
||||
|
||||
return <RcImage prefixCls={prefixCls} {...otherProps} />;
|
||||
const { locale: contextLocale = defaultLocale } = useContext(ConfigContext);
|
||||
const imageLocale = contextLocale.Image || defaultLocale.Image;
|
||||
|
||||
const mergedPreview = React.useMemo(() => {
|
||||
if (preview === false) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
return {
|
||||
mask: (
|
||||
<div className={`${prefixCls}-mask-info`}>
|
||||
<EyeOutlined />
|
||||
{imageLocale?.preview}
|
||||
</div>
|
||||
),
|
||||
...(typeof preview === 'object' ? preview : null),
|
||||
};
|
||||
}, [preview, imageLocale]);
|
||||
|
||||
return <RcImage prefixCls={prefixCls} preview={mergedPreview} {...otherProps} />;
|
||||
};
|
||||
|
||||
export { ImageProps };
|
||||
|
@ -20,6 +20,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: @text-color-inverse;
|
||||
background: fade(@black, 50%);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity @animation-duration-slow;
|
||||
|
||||
&-info {
|
||||
.@{iconfont-css-prefix} {
|
||||
margin-inline-end: @margin-xss;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
.box();
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-pr
|
||||
import SizeContext, { SizeType } from '../config-provider/SizeContext';
|
||||
import devWarning from '../_util/devWarning';
|
||||
|
||||
export interface InputFocusOptions extends FocusOptions {
|
||||
cursor?: 'start' | 'end' | 'all';
|
||||
}
|
||||
|
||||
export interface InputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'type'> {
|
||||
prefixCls?: string;
|
||||
@ -99,6 +103,34 @@ export function getInputClassName(
|
||||
});
|
||||
}
|
||||
|
||||
export function triggerFocus(
|
||||
element?: HTMLInputElement | HTMLTextAreaElement,
|
||||
option?: InputFocusOptions,
|
||||
) {
|
||||
if (!element) return;
|
||||
|
||||
element.focus(option);
|
||||
|
||||
// Selection content
|
||||
const { cursor } = option || {};
|
||||
if (cursor) {
|
||||
const len = element.value.length;
|
||||
|
||||
switch (cursor) {
|
||||
case 'start':
|
||||
element.setSelectionRange(0, 0);
|
||||
break;
|
||||
|
||||
case 'end':
|
||||
element.setSelectionRange(len, len);
|
||||
break;
|
||||
|
||||
default:
|
||||
element.setSelectionRange(0, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface InputState {
|
||||
value: any;
|
||||
focused: boolean;
|
||||
@ -171,8 +203,8 @@ class Input extends React.Component<InputProps, InputState> {
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
this.input.focus();
|
||||
focus = (option?: InputFocusOptions) => {
|
||||
triggerFocus(this.input, option);
|
||||
};
|
||||
|
||||
blur() {
|
||||
|
@ -1,24 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
|
||||
import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import ClearableLabeledInput from './ClearableLabeledInput';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { fixControlledValue, resolveOnChange } from './Input';
|
||||
import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input';
|
||||
import SizeContext, { SizeType } from '../config-provider/SizeContext';
|
||||
|
||||
interface ShowCountProps {
|
||||
formatter: (args: { count: number; maxLength?: number }) => string;
|
||||
}
|
||||
|
||||
export interface TextAreaProps extends RcTextAreaProps {
|
||||
allowClear?: boolean;
|
||||
bordered?: boolean;
|
||||
showCount?: boolean;
|
||||
showCount?: boolean | ShowCountProps;
|
||||
maxLength?: number;
|
||||
size?: SizeType;
|
||||
}
|
||||
|
||||
export interface TextAreaRef extends HTMLTextAreaElement {
|
||||
resizableTextArea: any;
|
||||
export interface TextAreaRef {
|
||||
focus: (options?: InputFocusOptions) => void;
|
||||
blur: () => void;
|
||||
resizableTextArea?: ResizableTextArea;
|
||||
}
|
||||
|
||||
const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
@ -38,7 +44,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const size = React.useContext(SizeContext);
|
||||
|
||||
const innerRef = React.useRef<TextAreaRef>();
|
||||
const innerRef = React.useRef<RcTextArea>();
|
||||
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
|
||||
|
||||
const [value, setValue] = useMergedState(props.defaultValue, {
|
||||
@ -63,18 +69,26 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
handleSetValue(e.target.value);
|
||||
resolveOnChange(innerRef.current!, e, props.onChange);
|
||||
resolveOnChange(innerRef.current as any, e, props.onChange);
|
||||
};
|
||||
|
||||
const handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
handleSetValue('', () => {
|
||||
innerRef.current?.focus();
|
||||
});
|
||||
resolveOnChange(innerRef.current!, e, props.onChange);
|
||||
resolveOnChange(innerRef.current as any, e, props.onChange);
|
||||
};
|
||||
|
||||
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
resizableTextArea: innerRef.current?.resizableTextArea,
|
||||
focus: (option?: InputFocusOptions) => {
|
||||
triggerFocus(innerRef.current?.resizableTextArea?.textArea, option);
|
||||
},
|
||||
blur: () => innerRef.current?.blur(),
|
||||
}));
|
||||
|
||||
const textArea = (
|
||||
<RcTextArea
|
||||
{...omit(props, ['allowClear'])}
|
||||
@ -88,14 +102,16 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
style={showCount ? null : style}
|
||||
prefixCls={prefixCls}
|
||||
onChange={handleChange}
|
||||
ref={composeRef(ref, innerRef)}
|
||||
ref={innerRef}
|
||||
/>
|
||||
);
|
||||
|
||||
const val = fixControlledValue(value) as string;
|
||||
let val = fixControlledValue(value) as string;
|
||||
|
||||
// Max length value
|
||||
const hasMaxLength = Number(maxLength) > 0;
|
||||
// fix #27612 将value转为数组进行截取,解决 '😂'.length === 2 等emoji表情导致的截取乱码的问题
|
||||
val = hasMaxLength ? [...val].slice(0, maxLength).join('') : val;
|
||||
|
||||
// TextArea
|
||||
const textareaNode = (
|
||||
@ -114,10 +130,14 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
|
||||
// Only show text area wrapper when needed
|
||||
if (showCount) {
|
||||
const valueLength = hasMaxLength
|
||||
? Math.min(Number(maxLength), [...val].length)
|
||||
: [...val].length;
|
||||
const dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
|
||||
const valueLength = [...val].length;
|
||||
|
||||
let dataCount = '';
|
||||
if (typeof showCount === 'object') {
|
||||
dataCount = showCount.formatter({ count: valueLength, maxLength });
|
||||
} else {
|
||||
dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -1184,6 +1184,104 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/focus.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus at first
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus at last
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus to select all
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus prevent scroll
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="ant-switch ant-switch-checked"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
Input
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
style="width:100%"
|
||||
type="text"
|
||||
value="Ant Design love you!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/input/demo/group.md correctly 1`] = `
|
||||
<div
|
||||
class="site-input-group-wrapper"
|
||||
|
58
components/input/__tests__/focus.test.tsx
Normal file
58
components/input/__tests__/focus.test.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
|
||||
import Input from '..';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
describe('Input.Focus', () => {
|
||||
let inputSpy: ReturnType<typeof spyElementPrototypes>;
|
||||
let textareaSpy: ReturnType<typeof spyElementPrototypes>;
|
||||
let focus: ReturnType<typeof jest.fn>;
|
||||
let setSelectionRange: ReturnType<typeof jest.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
focus = jest.fn();
|
||||
setSelectionRange = jest.fn();
|
||||
inputSpy = spyElementPrototypes(HTMLInputElement, {
|
||||
focus,
|
||||
setSelectionRange,
|
||||
});
|
||||
textareaSpy = spyElementPrototypes(HTMLTextAreaElement, {
|
||||
focus,
|
||||
setSelectionRange,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
inputSpy.mockRestore();
|
||||
textareaSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('start', () => {
|
||||
const ref = React.createRef<Input>();
|
||||
mount(<Input ref={ref} defaultValue="light" />);
|
||||
ref.current!.focus({ cursor: 'start' });
|
||||
|
||||
expect(focus).toHaveBeenCalled();
|
||||
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 0);
|
||||
});
|
||||
|
||||
it('end', () => {
|
||||
const ref = React.createRef<Input>();
|
||||
mount(<Input ref={ref} defaultValue="light" />);
|
||||
ref.current!.focus({ cursor: 'end' });
|
||||
|
||||
expect(focus).toHaveBeenCalled();
|
||||
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 5, 5);
|
||||
});
|
||||
|
||||
it('all', () => {
|
||||
const ref = React.createRef<any>();
|
||||
mount(<TextArea ref={ref} defaultValue="light" />);
|
||||
ref.current!.focus({ cursor: 'all' });
|
||||
|
||||
expect(focus).toHaveBeenCalled();
|
||||
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 5);
|
||||
});
|
||||
});
|
@ -140,7 +140,7 @@ describe('TextArea', () => {
|
||||
it('maxLength', () => {
|
||||
const wrapper = mount(<TextArea maxLength={5} showCount value="12345678" />);
|
||||
const textarea = wrapper.find('.ant-input-textarea');
|
||||
expect(wrapper.find('textarea').prop('value')).toBe('12345678');
|
||||
expect(wrapper.find('textarea').prop('value')).toBe('12345');
|
||||
expect(textarea.prop('data-count')).toBe('5 / 5');
|
||||
});
|
||||
|
||||
@ -165,6 +165,19 @@ describe('TextArea', () => {
|
||||
expect(wrapper.find('.ant-input').hasClass('bamboo')).toBeFalsy();
|
||||
expect(wrapper.find('.ant-input').props().style.background).toBeFalsy();
|
||||
});
|
||||
|
||||
it('count formatter', () => {
|
||||
const wrapper = mount(
|
||||
<TextArea
|
||||
maxLength={5}
|
||||
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }}
|
||||
value="12345678"
|
||||
/>,
|
||||
);
|
||||
const textarea = wrapper.find('.ant-input-textarea');
|
||||
expect(wrapper.find('textarea').prop('value')).toBe('12345');
|
||||
expect(textarea.prop('data-count')).toBe('5, 5');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support size', async () => {
|
||||
|
84
components/input/demo/focus.md
Normal file
84
components/input/demo/focus.md
Normal file
@ -0,0 +1,84 @@
|
||||
---
|
||||
order: 21
|
||||
title:
|
||||
zh-CN: 聚焦
|
||||
en-US: Focus
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
聚焦额外配置属性。
|
||||
|
||||
## en-US
|
||||
|
||||
Focus with additional option.
|
||||
|
||||
```tsx
|
||||
import { Input, Space, Button, Switch } from 'antd';
|
||||
|
||||
const Demo = () => {
|
||||
const inputRef = React.useRef<any>(null);
|
||||
const [input, setInput] = React.useState(true);
|
||||
|
||||
const sharedProps = {
|
||||
style: { width: '100%' },
|
||||
defaultValue: 'Ant Design love you!',
|
||||
ref: inputRef,
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
inputRef.current!.focus({
|
||||
cursor: 'start',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Focus at first
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
inputRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Focus at last
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
inputRef.current!.focus({
|
||||
cursor: 'all',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Focus to select all
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
inputRef.current!.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Focus prevent scroll
|
||||
</Button>
|
||||
<Switch
|
||||
checked={input}
|
||||
checkedChildren="Input"
|
||||
unCheckedChildren="TextArea"
|
||||
onChange={() => {
|
||||
setInput(!input);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{input ? <Input {...sharedProps} /> : <Input.TextArea {...sharedProps} />}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
@ -47,7 +47,7 @@ The rest of the props of Input are exactly the same as the original [input](http
|
||||
| bordered | Whether has border style | boolean | true | 4.5.0 |
|
||||
| defaultValue | The initial input content | string | - | |
|
||||
| maxLength | The max length | number | - | 4.7.0 |
|
||||
| showCount | Whether show text count | boolean | false | 4.7.0 |
|
||||
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) |
|
||||
| value | The input content value | string | - | |
|
||||
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
|
||||
| onResize | The callback function that is triggered when resize | function({ width, height }) | - | |
|
||||
@ -85,6 +85,13 @@ Supports all props of `Input`.
|
||||
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | Whether show toggle button | boolean | true | |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
| Name | Description | Parameters | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| blur | Remove focus | - | |
|
||||
| focus | Get focus | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why Input lose focus when change `prefix/suffix`
|
||||
|
@ -46,9 +46,10 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
|
||||
| allowClear | 可以点击清除图标删除内容 | boolean | false | |
|
||||
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false | |
|
||||
| bordered | 是否有边框 | boolean | true | 4.5.0 |
|
||||
| countFormatter | 指定字数展示的格式 | (count: number, maxLength?: number) => string | - | 4.10.0 |
|
||||
| defaultValue | 输入框默认内容 | string | - | |
|
||||
| maxLength | 内容最大长度 | number | - | 4.7.0 |
|
||||
| showCount | 是否展示字数 | boolean | false | 4.7.0 |
|
||||
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => string } | false | 4.7.0 (formatter: 4.10.0) |
|
||||
| value | 输入框内容 | string | - | |
|
||||
| onPressEnter | 按下回车的回调 | function(e) | - | |
|
||||
| onResize | resize 回调 | function({ width, height }) | - | |
|
||||
@ -86,6 +87,13 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
|
||||
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | 是否显示切换按钮 | boolean | true | |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
| 名称 | 说明 | 参数 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| blur | 取消焦点 | - | |
|
||||
| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
|
||||
|
||||
## FAQ
|
||||
|
||||
### 为什么我动态改变 `prefix/suffix` 时,Input 会失去焦点?
|
||||
|
@ -35,6 +35,9 @@ export interface Locale {
|
||||
optional?: string;
|
||||
defaultValidateMessages: ValidateMessages;
|
||||
};
|
||||
Image?: {
|
||||
preview: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocaleProviderProps {
|
||||
|
@ -24,6 +24,7 @@ const localeValues: Locale = {
|
||||
emptyText: 'No data',
|
||||
selectAll: 'Select current page',
|
||||
selectInvert: 'Invert current page',
|
||||
selectNone: 'Clear all data',
|
||||
selectionAll: 'Select all data',
|
||||
sortTitle: 'Sort',
|
||||
expand: 'Expand row',
|
||||
@ -125,6 +126,9 @@ const localeValues: Locale = {
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: 'Preview',
|
||||
},
|
||||
};
|
||||
|
||||
export default localeValues;
|
||||
|
@ -1,9 +1,12 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import Pagination from 'rc-pagination/lib/locale/hr_HR';
|
||||
import DatePicker from '../date-picker/locale/hr_HR';
|
||||
import TimePicker from '../time-picker/locale/hr_HR';
|
||||
import Calendar from '../calendar/locale/hr_HR';
|
||||
import { Locale } from '../locale-provider';
|
||||
|
||||
const typeTemplate = '${label} nije valjan ${type}';
|
||||
|
||||
const localeValues: Locale = {
|
||||
locale: 'hr',
|
||||
Pagination,
|
||||
@ -17,9 +20,17 @@ const localeValues: Locale = {
|
||||
filterTitle: 'Filter meni',
|
||||
filterConfirm: 'OK',
|
||||
filterReset: 'Reset',
|
||||
filterEmptyText: 'Nema filtera',
|
||||
emptyText: 'Nema podataka',
|
||||
selectAll: 'Označi trenutnu stranicu',
|
||||
selectInvert: 'Invertiraj trenutnu stranicu',
|
||||
selectionAll: 'Odaberite sve podatke',
|
||||
sortTitle: 'Sortiraj',
|
||||
expand: 'Proširi redak',
|
||||
collapse: 'Sažmi redak',
|
||||
triggerDesc: 'Kliknite za sortiranje silazno',
|
||||
triggerAsc: 'Kliknite za sortiranje uzlazno',
|
||||
cancelSort: 'Kliknite da biste otkazali sortiranje',
|
||||
},
|
||||
Modal: {
|
||||
okText: 'OK',
|
||||
@ -35,6 +46,12 @@ const localeValues: Locale = {
|
||||
searchPlaceholder: 'Pretraži ovdje',
|
||||
itemUnit: 'stavka',
|
||||
itemsUnit: 'stavke',
|
||||
remove: 'Ukloniti',
|
||||
selectCurrent: 'Odaberite trenutnu stranicu',
|
||||
removeCurrent: 'Ukloni trenutnu stranicu',
|
||||
selectAll: 'Odaberite sve podatke',
|
||||
removeAll: 'Uklonite sve podatke',
|
||||
selectInvert: 'Obrni trenutnu stranicu',
|
||||
},
|
||||
Upload: {
|
||||
uploading: 'Upload u tijeku...',
|
||||
@ -50,10 +67,66 @@ const localeValues: Locale = {
|
||||
icon: 'ikona',
|
||||
},
|
||||
Text: {
|
||||
edit: 'uredi',
|
||||
copy: 'kopiraj',
|
||||
copied: 'kopiranje uspješno',
|
||||
expand: 'proširi',
|
||||
edit: 'Uredi',
|
||||
copy: 'Kopiraj',
|
||||
copied: 'Kopiranje uspješno',
|
||||
expand: 'Proširi',
|
||||
},
|
||||
PageHeader: {
|
||||
back: 'Natrag',
|
||||
},
|
||||
Form: {
|
||||
optional: '(neobavezno)',
|
||||
defaultValidateMessages: {
|
||||
default: 'Pogreška provjere valjanosti polja za ${label}',
|
||||
required: 'Molimo unesite ${label}',
|
||||
enum: '${label} mora biti jedan od [${enum}]',
|
||||
whitespace: '${label} ne može biti prazan znak',
|
||||
date: {
|
||||
format: '${label} format datuma je nevažeći',
|
||||
parse: '${label} ne može se pretvoriti u datum',
|
||||
invalid: '${label} je nevažeći datum',
|
||||
},
|
||||
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} mora biti ${len} slova',
|
||||
min: '${label} mora biti najmanje ${min} slova',
|
||||
max: '${label} mora biti do ${max} slova',
|
||||
range: '${label} mora biti između ${min}-${max} slova',
|
||||
},
|
||||
number: {
|
||||
len: '${label} mora biti jednak ${len}',
|
||||
min: '${label} mora biti minimalano ${min}',
|
||||
max: '${label} mora biti maksimalano ${max}',
|
||||
range: '${label} mora biti između ${min}-${max}',
|
||||
},
|
||||
array: {
|
||||
len: 'Mora biti ${len} ${label}',
|
||||
min: 'Najmanje ${min} ${label}',
|
||||
max: 'Najviše ${max} ${label}',
|
||||
range: 'Količina ${label} mora biti između ${min}-${max}',
|
||||
},
|
||||
pattern: {
|
||||
mismatch: '${label} ne odgovara obrascu ${pattern}',
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: 'Pregled',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,7 @@ const localeValues: Locale = {
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
Calendar,
|
||||
// locales for all comoponents
|
||||
// locales for all components
|
||||
global: {
|
||||
placeholder: '请选择',
|
||||
},
|
||||
@ -24,6 +24,7 @@ const localeValues: Locale = {
|
||||
filterEmptyText: '无筛选项',
|
||||
selectAll: '全选当页',
|
||||
selectInvert: '反选当页',
|
||||
selectNone: '清空所有',
|
||||
selectionAll: '全选所有',
|
||||
sortTitle: '排序',
|
||||
expand: '展开行',
|
||||
@ -124,6 +125,9 @@ const localeValues: Locale = {
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: '预览',
|
||||
},
|
||||
};
|
||||
|
||||
export default localeValues;
|
||||
|
@ -23,6 +23,7 @@ const localeValues: Locale = {
|
||||
filterEmptyText: '無篩選項',
|
||||
selectAll: '全部選取',
|
||||
selectInvert: '反向選取',
|
||||
selectNone: '清空所有',
|
||||
selectionAll: '全選所有',
|
||||
sortTitle: '排序',
|
||||
expand: '展開行',
|
||||
|
@ -23,6 +23,7 @@ const localeValues: Locale = {
|
||||
filterEmptyText: '無篩選項',
|
||||
selectAll: '全部選取',
|
||||
selectInvert: '反向選取',
|
||||
selectNone: '清空所有',
|
||||
selectionAll: '全選所有',
|
||||
sortTitle: '排序',
|
||||
expand: '展開行',
|
||||
|
@ -65,6 +65,27 @@ describe('message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger onClick method', () => {
|
||||
const onClick = jest.fn();
|
||||
class Test extends React.Component {
|
||||
componentDidMount() {
|
||||
message.info({
|
||||
onClick,
|
||||
duration: 0,
|
||||
content: 'message info',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>test message onClick method</div>;
|
||||
}
|
||||
}
|
||||
mount(<Test />);
|
||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||
document.querySelectorAll('.ant-message-notice')[0].click();
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be called like promise', done => {
|
||||
jest.useRealTimers();
|
||||
const defaultDuration = 3;
|
||||
|
@ -58,6 +58,7 @@ The properties of config are as follows:
|
||||
| key | The unique identifier of the Message | string \| number | - |
|
||||
| style | Customized inline style | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - |
|
||||
| onClose | Specify a function that will be called when the message is closed | function | - |
|
||||
| onClick | Specify a function that will be called when the message is clicked | function | - |
|
||||
|
||||
### Global static methods
|
||||
|
||||
|
@ -127,6 +127,7 @@ export interface ArgsProps {
|
||||
key?: string | number;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
|
||||
@ -148,6 +149,7 @@ function getRCNoticeProps(args: ArgsProps, prefixCls: string): NoticeContent {
|
||||
</div>
|
||||
),
|
||||
onClose: args.onClose,
|
||||
onClick: args.onClick,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
|
||||
| key | 当前提示的唯一标志 | string \| number | - |
|
||||
| style | 自定义内联样式 | [CSSProperties](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) | - |
|
||||
| onClose | 关闭时触发的回调函数 | function | - |
|
||||
| onClick | 点击 message 时触发的回调函数 | function | - |
|
||||
|
||||
### 全局方法
|
||||
|
||||
|
@ -95,6 +95,7 @@ export interface ModalFuncProps {
|
||||
// TODO: find out exact types
|
||||
onOk?: (...args: any[]) => any;
|
||||
onCancel?: (...args: any[]) => any;
|
||||
afterClose?: () => void;
|
||||
okButtonProps?: ButtonProps;
|
||||
cancelButtonProps?: ButtonProps;
|
||||
centered?: boolean;
|
||||
|
@ -497,4 +497,28 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('trigger afterClose once when click on cancel button', async () => {
|
||||
const afterClose = jest.fn();
|
||||
open({
|
||||
afterClose,
|
||||
});
|
||||
// first Modal
|
||||
$$('.ant-btn')[0].click();
|
||||
expect(afterClose).not.toHaveBeenCalled();
|
||||
await sleep(500);
|
||||
expect(afterClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('trigger afterClose once when click on ok button', async () => {
|
||||
const afterClose = jest.fn();
|
||||
open({
|
||||
afterClose,
|
||||
});
|
||||
// second Modal
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect(afterClose).not.toHaveBeenCalled();
|
||||
await sleep(500);
|
||||
expect(afterClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -82,7 +82,12 @@ export default function confirm(config: ModalFuncProps) {
|
||||
currentConfig = {
|
||||
...currentConfig,
|
||||
visible: false,
|
||||
afterClose: destroy.bind(this, ...args),
|
||||
afterClose: () => {
|
||||
if (typeof config.afterClose === 'function') {
|
||||
config.afterClose();
|
||||
}
|
||||
destroy.apply(this, args);
|
||||
},
|
||||
};
|
||||
render(currentConfig);
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ The items listed above are all functions, expecting a settings object as paramet
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| afterClose | Specify a function that will be called when modal is closed completely | function | - | 4.9.0 |
|
||||
| autoFocusButton | Specify which button to autofocus | null \| `ok` \| `cancel` | `ok` | |
|
||||
| bodyStyle | Body style for modal body element. Such as height, padding etc | CSSProperties | | 4.8.0 |
|
||||
| cancelButtonProps | The cancel button props | [ButtonProps](/components/button/#API) | - | |
|
||||
|
@ -68,6 +68,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| afterClose | Modal 完全关闭后的回调 | function | - | 4.9.0 |
|
||||
| autoFocusButton | 指定自动获得焦点的按钮 | null \| `ok` \| `cancel` | `ok` | |
|
||||
| bodyStyle | Modal body 样式 | CSSProperties | | 4.8.0 |
|
||||
| cancelButtonProps | cancel 按钮 props | [ButtonProps](/components/button/#API) | - | |
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 23
|
||||
order: 25
|
||||
title:
|
||||
zh-CN: 大数据
|
||||
en-US: Big Data
|
||||
|
58
components/select/demo/responsive.md
Normal file
58
components/select/demo/responsive.md
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
order: 24
|
||||
title:
|
||||
zh-CN: 响应式 maxTagCount
|
||||
en-US: Responsive maxTagCount
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
多选下通过响应式布局让选项自动收缩。该功能对性能有所消耗,不推荐在大表单场景下使用。
|
||||
|
||||
## en-US
|
||||
|
||||
Auto collapse to tag with responsive case. Not recommend use in large form case since responsive calculation has a perf cost.
|
||||
|
||||
```tsx
|
||||
import { Select, Space } from 'antd';
|
||||
|
||||
interface ItemProps {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const options: ItemProps[] = [];
|
||||
|
||||
for (let i = 10; i < 36; i++) {
|
||||
const value = i.toString(36) + i;
|
||||
options.push({
|
||||
label: `Long Label: ${value}`,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
const Demo = () => {
|
||||
const [value, setValue] = React.useState(['a10', 'c12', 'h17', 'j19', 'k20']);
|
||||
|
||||
const selectProps = {
|
||||
mode: 'multiple' as const,
|
||||
style: { width: '100%' },
|
||||
value,
|
||||
options,
|
||||
onChange: (newValue: string[]) => {
|
||||
setValue(newValue);
|
||||
},
|
||||
placeholder: 'Select Item...',
|
||||
maxTagCount: 'responsive' as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Select {...selectProps} />
|
||||
<Select {...selectProps} disabled />
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
@ -43,7 +43,7 @@ Select component to select value from options.
|
||||
| labelInValue | Whether to embed label in value, turn the format of value from `string` to { value: string, label: ReactNode } | boolean | false | |
|
||||
| listHeight | Config popup height | number | 256 | |
|
||||
| loading | Indicate loading state | boolean | false | |
|
||||
| maxTagCount | Max tag count to show | number | - | |
|
||||
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 |
|
||||
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | |
|
||||
| maxTagTextLength | Max tag text length to show | number | - | |
|
||||
| menuItemSelectedIcon | The custom menuItemSelected icon with multiple options | ReactNode | - | |
|
||||
|
@ -44,7 +44,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
|
||||
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 { value: string, label: ReactNode } 的格式 | boolean | false | |
|
||||
| listHeight | 设置弹窗滚动高度 | number | 256 | |
|
||||
| loading | 加载中状态 | boolean | false | |
|
||||
| maxTagCount | 最多显示多少个 tag | number | - | |
|
||||
| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
|
||||
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
|
||||
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
|
||||
| menuItemSelectedIcon | 自定义多选时当前选中的条目图标 | ReactNode | - | |
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import './index';
|
||||
|
||||
@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
|
||||
@select-multiple-item-border-width: 1px;
|
||||
|
||||
@select-multiple-padding: max(
|
||||
@ -13,6 +14,20 @@
|
||||
* since chrome may update to redesign with its align logic.
|
||||
*/
|
||||
|
||||
// =========================== Overflow ===========================
|
||||
.@{select-overflow-prefix-cls} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: auto;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
|
||||
&-item {
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix-cls} {
|
||||
&-multiple {
|
||||
// ========================= Selector =========================
|
||||
@ -56,9 +71,10 @@
|
||||
|
||||
height: @select-multiple-item-height;
|
||||
margin-top: @select-multiple-item-spacing-half;
|
||||
margin-right: @input-padding-vertical-base;
|
||||
margin-inline-end: @input-padding-vertical-base;
|
||||
margin-bottom: @select-multiple-item-spacing-half;
|
||||
padding: 0 (@padding-xs / 2) 0 @padding-xs;
|
||||
padding-inline-start: @padding-xs;
|
||||
padding-inline-end: (@padding-xs / 2);
|
||||
line-height: @select-multiple-item-height - @select-multiple-item-border-width * 2;
|
||||
background: @select-selection-item-bg;
|
||||
border: 1px solid @select-selection-item-border-color;
|
||||
@ -102,14 +118,24 @@
|
||||
}
|
||||
|
||||
// ========================== Input ==========================
|
||||
.@{select-overflow-prefix-cls}-item + .@{select-overflow-prefix-cls}-item {
|
||||
.@{select-prefix-cls}-selection-search {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix-cls}-selection-search {
|
||||
position: relative;
|
||||
margin-left: (@select-multiple-padding / 2);
|
||||
max-width: 100%;
|
||||
margin-top: @select-multiple-item-spacing-half;
|
||||
margin-bottom: @select-multiple-item-spacing-half;
|
||||
margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
|
||||
|
||||
&-input,
|
||||
&-mirror {
|
||||
height: @select-multiple-item-height;
|
||||
font-family: @font-family;
|
||||
line-height: @line-height-base;
|
||||
line-height: @select-multiple-item-height;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@ -126,11 +152,6 @@
|
||||
white-space: pre; // fix whitespace wrapping caused width calculation bug
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/22906
|
||||
&:first-child > .@{select-prefix-cls}-selection-search-input {
|
||||
margin-left: 6.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= Placeholder =======================
|
||||
|
@ -66,9 +66,6 @@
|
||||
// ======================== Selections ========================
|
||||
.@{select-prefix-cls}-selection-item {
|
||||
.@{select-prefix-cls}-rtl& {
|
||||
margin-right: 0;
|
||||
margin-left: @input-padding-vertical-base;
|
||||
padding: 0 @padding-xs 0 (@padding-xs / 2);
|
||||
text-align: right;
|
||||
}
|
||||
// It's ok not to do this, but 24px makes bottom narrow in view should adjust
|
||||
@ -83,11 +80,6 @@
|
||||
|
||||
// ========================== Input ==========================
|
||||
.@{select-prefix-cls}-selection-search {
|
||||
.@{select-prefix-cls}-rtl& {
|
||||
margin-right: (@select-multiple-padding / 2);
|
||||
margin-left: @input-padding-vertical-base;
|
||||
}
|
||||
|
||||
&-mirror {
|
||||
.@{select-prefix-cls}-rtl& {
|
||||
right: 0;
|
||||
|
@ -83,6 +83,46 @@ Array [
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/slider/demo/dragableTrack.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-slider"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track ant-slider-track-1"
|
||||
style="left:20%;right:auto;width:30%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="20"
|
||||
class="ant-slider-handle ant-slider-handle-1"
|
||||
role="slider"
|
||||
style="left:20%;right:auto;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="ant-slider-handle ant-slider-handle-2"
|
||||
role="slider"
|
||||
style="left:50%;right:auto;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-mark"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/slider/demo/event.md correctly 1`] = `
|
||||
Array [
|
||||
<div
|
||||
|
20
components/slider/demo/dragableTrack.md
Normal file
20
components/slider/demo/dragableTrack.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
order: 9
|
||||
title:
|
||||
zh-CN: 范围可拖拽
|
||||
en-US: Draggable track
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以设置 `range.draggableTrack`,使得范围刻度整体可拖拽。
|
||||
|
||||
## en-US
|
||||
|
||||
Make range track draggable when set `range.draggableTrack`.
|
||||
|
||||
```jsx
|
||||
import { Slider } from 'antd';
|
||||
|
||||
ReactDOM.render(<Slider range={{ draggableTrack: true }} defaultValue={[20, 50]} />, mountNode);
|
||||
```
|
@ -35,6 +35,12 @@ To input a value in a range.
|
||||
| onAfterChange | Fire when onmouseup is fired | (value) => void | - | |
|
||||
| onChange | Callback function that is fired when the user changes the slider's value | (value) => void | - | |
|
||||
|
||||
### range
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| draggableTrack | Whether range track can be drag | boolean | false | 4.10.0 |
|
||||
|
||||
## Methods
|
||||
|
||||
| Name | Description | Version |
|
||||
|
@ -59,7 +59,7 @@ export interface SliderSingleProps extends SliderBaseProps {
|
||||
}
|
||||
|
||||
export interface SliderRangeProps extends SliderBaseProps {
|
||||
range: true;
|
||||
range: true | SliderRange;
|
||||
value?: [number, number];
|
||||
defaultValue?: [number, number];
|
||||
onChange?: (value: [number, number]) => void;
|
||||
@ -68,6 +68,10 @@ export interface SliderRangeProps extends SliderBaseProps {
|
||||
trackStyle?: React.CSSProperties[];
|
||||
}
|
||||
|
||||
interface SliderRange {
|
||||
draggableTrack?: boolean;
|
||||
}
|
||||
|
||||
export type Visibles = { [index: number]: boolean };
|
||||
|
||||
const Slider = React.forwardRef<unknown, SliderSingleProps | SliderRangeProps>(
|
||||
@ -136,15 +140,24 @@ const Slider = React.forwardRef<unknown, SliderSingleProps | SliderRangeProps>(
|
||||
const cls = classNames(className, {
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
});
|
||||
|
||||
// make reverse default on rtl direction
|
||||
if (direction === 'rtl' && !restProps.vertical) {
|
||||
restProps.reverse = !restProps.reverse;
|
||||
}
|
||||
|
||||
// extrack draggableTrack from range={{ ... }}
|
||||
let draggableTrack: boolean | undefined;
|
||||
if (typeof range === 'object') {
|
||||
draggableTrack = range.draggableTrack;
|
||||
}
|
||||
|
||||
if (range) {
|
||||
return (
|
||||
<RcRange
|
||||
{...(restProps as SliderRangeProps)}
|
||||
step={restProps.step!}
|
||||
draggableTrack={draggableTrack}
|
||||
className={cls}
|
||||
ref={ref}
|
||||
handle={(info: HandleGeneratorInfo) =>
|
||||
|
@ -25,7 +25,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/HZ3meFc6W/Silder.svg
|
||||
| marks | 刻度标记,key 的类型必须为 `number` 且取值在闭区间 \[min, max] 内,每个标签可以单独设置样式 | object | { number: ReactNode } or { number: { style: object, label: ReactNode } } | |
|
||||
| max | 最大值 | number | 100 | |
|
||||
| min | 最小值 | number | 0 | |
|
||||
| range | 双滑块模式 | boolean | false | |
|
||||
| range | 双滑块模式 | boolean \| [range](#range) | false | |
|
||||
| reverse | 反向坐标轴 | boolean | false | |
|
||||
| step | 步长,取值必须大于 0,并且可被 (max - min) 整除。当 `marks` 不为空对象时,可以设置 `step` 为 null,此时 Slider 的可选值仅有 marks 标出来的部分 | number \| null | 1 | |
|
||||
| tipFormatter | Slider 会把当前值传给 `tipFormatter`,并在 Tooltip 中显示 `tipFormatter` 的返回值,若为 null,则隐藏 Tooltip | value => ReactNode \| null | IDENTITY | |
|
||||
@ -36,6 +36,12 @@ cover: https://gw.alipayobjects.com/zos/alicdn/HZ3meFc6W/Silder.svg
|
||||
| onAfterChange | 与 `onmouseup` 触发时机一致,把当前值作为参数传入 | (value) => void | - | |
|
||||
| onChange | 当 Slider 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入 | (value) => void | - | |
|
||||
|
||||
### range
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| draggableTrack | 范围刻度是否可被拖拽 | boolean | false | 4.10.0 |
|
||||
|
||||
## 方法
|
||||
|
||||
| 名称 | 描述 | 版本 |
|
||||
|
@ -35,6 +35,7 @@ The whole of the step bar.
|
||||
| labelPlacement | Place title and description with `horizontal` or `vertical` direction | string | `horizontal` | |
|
||||
| percent | Progress circle percentage of current step in `process` status (only works on basic Steps) | number | - | 4.5.0 |
|
||||
| progressDot | Steps with progress dot style, customize the progress dot by setting it to a function. labelPlacement will be `vertical` | boolean \| (iconDot, {index, status, title, description}) => ReactNode | false | |
|
||||
| responsive | change to vertical direction when screen width smaller than `532px` | boolean | - | true |
|
||||
| size | To specify the size of the step bar, `default` and `small` are currently supported | string | `default` | |
|
||||
| status | To specify the status of current step, can be set to one of the following values: `wait` `process` `finish` `error` | string | `process` | |
|
||||
| type | Type of steps, can be set to one of the following values: `default`, `navigation` | string | `default` | |
|
||||
|
@ -18,6 +18,7 @@ export interface StepsProps {
|
||||
labelPlacement?: 'horizontal' | 'vertical';
|
||||
prefixCls?: string;
|
||||
progressDot?: boolean | Function;
|
||||
responsive?: boolean;
|
||||
size?: 'default' | 'small';
|
||||
status?: 'wait' | 'process' | 'finish' | 'error';
|
||||
style?: React.CSSProperties;
|
||||
@ -42,11 +43,14 @@ interface StepsType extends React.FC<StepsProps> {
|
||||
}
|
||||
|
||||
const Steps: StepsType = props => {
|
||||
const { percent, size, className, direction } = props;
|
||||
const { percent, size, className, direction, responsive } = props;
|
||||
const { xs } = useBreakpoint();
|
||||
const { getPrefixCls, direction: rtlDirection } = React.useContext(ConfigContext);
|
||||
|
||||
const getDirection = React.useCallback(() => (xs ? 'vertical' : direction), [xs, direction]);
|
||||
const getDirection = React.useCallback(() => (responsive && xs ? 'vertical' : direction), [
|
||||
xs,
|
||||
direction,
|
||||
]);
|
||||
|
||||
const prefixCls = getPrefixCls('steps', props.prefixCls);
|
||||
const iconPrefix = getPrefixCls('', props.iconPrefix);
|
||||
|
@ -36,6 +36,7 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/UZYqMizXHaj/Steps.svg
|
||||
| labelPlacement | 指定标签放置位置,默认水平放图标右侧,可选 `vertical` 放图标下方 | string | `horizontal` | |
|
||||
| percent | 当前 `process` 步骤显示的进度条进度(只对基本类型的 Steps 生效) | number | - | 4.5.0 |
|
||||
| progressDot | 点状步骤条,可以设置为一个 function,labelPlacement 将强制为 `vertical` | boolean \| (iconDot, {index, status, title, description}) => ReactNode | false | |
|
||||
| responsive | 当屏幕宽度小于 532px 时自动变为垂直模式 | boolean | - | true |
|
||||
| size | 指定大小,目前支持普通(`default`)和迷你(`small`) | string | `default` | |
|
||||
| status | 指定当前步骤的状态,可选 `wait` `process` `finish` `error` | string | `process` | |
|
||||
| type | 步骤条类型,有 `default` 和 `navigation` 两种 | string | `default` | |
|
||||
|
@ -90,35 +90,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs) {
|
||||
.@{steps-prefix-cls}-navigation {
|
||||
> .@{steps-prefix-cls}-item {
|
||||
margin-right: 0 !important;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
&.@{steps-prefix-cls}-item-active::before {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: unset;
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: calc(100% - 24px);
|
||||
}
|
||||
&::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 50%;
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
> .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
|
||||
visibility: hidden;
|
||||
}
|
||||
.@{steps-prefix-cls}-navigation.@{steps-prefix-cls}-vertical {
|
||||
> .@{steps-prefix-cls}-item {
|
||||
margin-right: 0 !important;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
&.@{steps-prefix-cls}-item-active::before {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: unset;
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: calc(100% - 24px);
|
||||
}
|
||||
&::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 50%;
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
> .@{steps-prefix-cls}-item-container > .@{steps-prefix-cls}-item-tail {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1006,6 +1006,7 @@
|
||||
@image-font-size-base: 24px;
|
||||
@image-bg: #f5f5f5;
|
||||
@image-color: #fff;
|
||||
@image-mask-font-size: 16px;
|
||||
@image-preview-operation-size: 18px;
|
||||
@image-preview-operation-color: @text-color-dark;
|
||||
@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%);
|
||||
|
@ -26,7 +26,11 @@ import {
|
||||
TableLocale,
|
||||
TableAction,
|
||||
} from './interface';
|
||||
import useSelection, { SELECTION_ALL, SELECTION_INVERT } from './hooks/useSelection';
|
||||
import useSelection, {
|
||||
SELECTION_ALL,
|
||||
SELECTION_INVERT,
|
||||
SELECTION_NONE,
|
||||
} from './hooks/useSelection';
|
||||
import useSorter, { getSortData, SortState } from './hooks/useSorter';
|
||||
import useFilter, { getFilterData, FilterState } from './hooks/useFilter';
|
||||
import useTitleColumns from './hooks/useTitleColumns';
|
||||
@ -509,6 +513,7 @@ Table.defaultProps = {
|
||||
|
||||
Table.SELECTION_ALL = SELECTION_ALL;
|
||||
Table.SELECTION_INVERT = SELECTION_INVERT;
|
||||
Table.SELECTION_NONE = SELECTION_NONE;
|
||||
Table.Column = Column;
|
||||
Table.ColumnGroup = ColumnGroup;
|
||||
Table.Summary = Summary;
|
||||
|
@ -294,11 +294,28 @@ describe('Table.rowSelection', () => {
|
||||
checkboxes.at(1).simulate('change', { target: { checked: true } });
|
||||
|
||||
const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item').last().simulate('click');
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item').at(1).simulate('click');
|
||||
|
||||
expect(handleSelectInvert).toHaveBeenCalledWith([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('fires selectNone event', () => {
|
||||
const handleSelectNone = jest.fn();
|
||||
const rowSelection = {
|
||||
onSelectNone: handleSelectNone,
|
||||
selections: true,
|
||||
};
|
||||
const wrapper = mount(createTable({ rowSelection }));
|
||||
const checkboxes = wrapper.find('input');
|
||||
|
||||
checkboxes.at(1).simulate('change', { target: { checked: true } });
|
||||
|
||||
const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
|
||||
dropdownWrapper.find('.ant-dropdown-menu-item').last().simulate('click');
|
||||
|
||||
expect(handleSelectNone).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires selection event', () => {
|
||||
const handleSelectOdd = jest.fn();
|
||||
const handleSelectEven = jest.fn();
|
||||
|
@ -984,6 +984,12 @@ exports[`Table.rowSelection render with default selection correctly 1`] = `
|
||||
>
|
||||
Invert current page
|
||||
</li>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
Clear all data
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -1093,6 +1099,12 @@ exports[`Table.rowSelection should support getPopupContainer 1`] = `
|
||||
>
|
||||
Invert current page
|
||||
</li>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
Clear all data
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -1420,6 +1432,12 @@ exports[`Table.rowSelection should support getPopupContainer from ConfigProvider
|
||||
>
|
||||
Invert current page
|
||||
</li>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
Clear all data
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,6 +59,7 @@ class App extends React.Component {
|
||||
selections: [
|
||||
Table.SELECTION_ALL,
|
||||
Table.SELECTION_INVERT,
|
||||
Table.SELECTION_NONE,
|
||||
{
|
||||
key: 'odd',
|
||||
text: 'Select Odd Row',
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
// TODO: warning if use ajax!!!
|
||||
export const SELECTION_ALL = 'SELECT_ALL' as const;
|
||||
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
|
||||
export const SELECTION_NONE = 'SELECT_NONE' as const;
|
||||
|
||||
function getFixedType<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
|
||||
return column && column.fixed;
|
||||
@ -49,7 +50,8 @@ interface UseSelectionConfig<RecordType> {
|
||||
export type INTERNAL_SELECTION_ITEM =
|
||||
| SelectionItem
|
||||
| typeof SELECTION_ALL
|
||||
| typeof SELECTION_INVERT;
|
||||
| typeof SELECTION_INVERT
|
||||
| typeof SELECTION_NONE;
|
||||
|
||||
function flattenData<RecordType>(
|
||||
data: RecordType[] | undefined,
|
||||
@ -82,6 +84,7 @@ export default function useSelection<RecordType>(
|
||||
onSelect,
|
||||
onSelectAll,
|
||||
onSelectInvert,
|
||||
onSelectNone,
|
||||
onSelectMultiple,
|
||||
columnWidth: selectionColWidth,
|
||||
type: selectionType,
|
||||
@ -255,7 +258,7 @@ export default function useSelection<RecordType>(
|
||||
}
|
||||
|
||||
const selectionList: INTERNAL_SELECTION_ITEM[] =
|
||||
selections === true ? [SELECTION_ALL, SELECTION_INVERT] : selections;
|
||||
selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections;
|
||||
|
||||
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => {
|
||||
if (selection === SELECTION_ALL) {
|
||||
@ -296,6 +299,18 @@ export default function useSelection<RecordType>(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (selection === SELECTION_NONE) {
|
||||
return {
|
||||
key: 'none',
|
||||
text: tableLocale.selectNone,
|
||||
onSelect() {
|
||||
setSelectedKeys([]);
|
||||
if (onSelectNone) {
|
||||
onSelectNone();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return selection as SelectionItem;
|
||||
});
|
||||
}, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
|
||||
|
@ -166,7 +166,8 @@ More about pagination, please check [`Pagination`](/components/pagination/).
|
||||
Properties for expandable.
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| columnWidth | Set the width of the expand column | string \| number | - | |
|
||||
| childrenColumnName | The column contains children to display | string | children |
|
||||
| defaultExpandAllRows | Expand all rows initially | boolean | false |
|
||||
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - |
|
||||
@ -202,6 +203,7 @@ Properties for row selection.
|
||||
| onSelect | Callback executed when select/deselect one row | function(record, selected, selectedRows, nativeEvent) | - | |
|
||||
| onSelectAll | Callback executed when select/deselect all rows | function(selected, selectedRows, changeRows) | - | |
|
||||
| onSelectInvert | Callback executed when row selection is inverted | function(selectedRowKeys) | - | |
|
||||
| onSelectNone | Callback executed when row selection is cleared | function() | - | |
|
||||
|
||||
### scroll
|
||||
|
||||
|
@ -173,7 +173,8 @@ const columns = [
|
||||
展开功能的配置。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| columnWidth | 自定义展开列宽度 | string \| number | - | |
|
||||
| childrenColumnName | 指定树形结构的列名 | string | children |
|
||||
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false |
|
||||
| defaultExpandedRowKeys | 默认展开的行 | string\[] | - |
|
||||
@ -209,6 +210,7 @@ const columns = [
|
||||
| onSelect | 用户手动选择/取消选择某行的回调 | function(record, selected, selectedRows, nativeEvent) | - | |
|
||||
| onSelectAll | 用户手动选择/取消选择所有行的回调 | function(selected, selectedRows, changeRows) | - | |
|
||||
| onSelectInvert | 用户手动选择反选的回调 | function(selectedRowKeys) | - | |
|
||||
| onSelectNone | 用户清空选择的回调 | function() | - | |
|
||||
|
||||
### scroll
|
||||
|
||||
|
@ -29,6 +29,7 @@ export interface TableLocale {
|
||||
filterEmptyText?: React.ReactNode;
|
||||
emptyText?: React.ReactNode | (() => React.ReactNode);
|
||||
selectAll?: React.ReactNode;
|
||||
selectNone?: React.ReactNode;
|
||||
selectInvert?: React.ReactNode;
|
||||
selectionAll?: React.ReactNode;
|
||||
sortTitle?: string;
|
||||
@ -143,6 +144,7 @@ export interface TableRowSelection<T> {
|
||||
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
|
||||
/** @deprecated This function is meaningless and should use `onChange` instead */
|
||||
onSelectInvert?: (selectedRowKeys: Key[]) => void;
|
||||
onSelectNone?: () => void;
|
||||
selections?: INTERNAL_SELECTION_ITEM[] | boolean;
|
||||
hideSelectAll?: boolean;
|
||||
fixed?: boolean;
|
||||
|
@ -2,6 +2,7 @@ import { TimePickerLocale } from '../index';
|
||||
|
||||
const locale: TimePickerLocale = {
|
||||
placeholder: 'Odaberite vrijeme',
|
||||
rangePlaceholder: ['Vrijeme početka', 'Vrijeme završetka'],
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
@ -129,67 +129,81 @@ exports[`renders ./components/tree-select/demo/checkable.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-select-selector"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item"
|
||||
<div
|
||||
class="ant-select-selection-overflow"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
>
|
||||
Node1
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select:none;-webkit-user-select:none"
|
||||
unselectable="on"
|
||||
<div
|
||||
class="ant-select-selection-overflow-item"
|
||||
style="opacity:1"
|
||||
>
|
||||
<span
|
||||
aria-label="close"
|
||||
class="anticon anticon-close"
|
||||
role="img"
|
||||
class="ant-select-selection-item"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
>
|
||||
<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>
|
||||
Node1
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select:none;-webkit-user-select:none"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="close"
|
||||
class="anticon anticon-close"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
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>
|
||||
<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"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
</div>
|
||||
<div
|
||||
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
|
||||
style="opacity:1"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
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"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -203,32 +217,41 @@ exports[`renders ./components/tree-select/demo/multiple.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-select-selector"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-search"
|
||||
style="width:0"
|
||||
<div
|
||||
class="ant-select-selection-overflow"
|
||||
>
|
||||
<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"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
<div
|
||||
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
|
||||
style="opacity:1"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
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"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="ant-select-selection-placeholder"
|
||||
>
|
||||
|
@ -150,71 +150,90 @@ exports[`TreeSelect TreeSelect Custom Icons should support customized icons 1`]
|
||||
<div
|
||||
class="ant-select-selector"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item"
|
||||
<div
|
||||
class="ant-select-selection-overflow"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
<div
|
||||
class="ant-select-selection-overflow-item"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
my leaf
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select: none;"
|
||||
unselectable="on"
|
||||
>
|
||||
<span>
|
||||
remove
|
||||
<span
|
||||
class="ant-select-selection-item"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
>
|
||||
my leaf
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select: none;"
|
||||
unselectable="on"
|
||||
>
|
||||
<span>
|
||||
remove
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="ant-select-selection-item"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
</div>
|
||||
<div
|
||||
class="ant-select-selection-overflow-item"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
your leaf
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select: none;"
|
||||
unselectable="on"
|
||||
>
|
||||
<span>
|
||||
remove
|
||||
<span
|
||||
class="ant-select-selection-item"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-item-content"
|
||||
>
|
||||
your leaf
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-item-remove"
|
||||
style="user-select: none;"
|
||||
unselectable="on"
|
||||
>
|
||||
<span>
|
||||
remove
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="ant-select-selection-search"
|
||||
style="width: 0px;"
|
||||
>
|
||||
<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"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity: 0;"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
</div>
|
||||
<div
|
||||
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="ant-select-selection-search"
|
||||
style="width: 0px;"
|
||||
>
|
||||
<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"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity: 0;"
|
||||
type="search"
|
||||
unselectable="on"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="ant-select-selection-search-mirror"
|
||||
>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
|
@ -31,7 +31,7 @@ Tree selection control.
|
||||
| labelInValue | Whether to embed label in value, turn the format of value from `string` to {value: string, label: ReactNode, halfChecked: string\[]} | boolean | false | |
|
||||
| listHeight | Config popup height | number | 256 | |
|
||||
| loadData | Load data asynchronously | function(node) | - | |
|
||||
| maxTagCount | Max tag count to show | number | - | |
|
||||
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 |
|
||||
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | |
|
||||
| multiple | Support multiple or not, will be `true` when enable `treeCheckable` | boolean | false | |
|
||||
| placeholder | Placeholder of the select input | string | - | |
|
||||
|
@ -32,7 +32,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
|
||||
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 value 类型从 `string` 变为 {value: string, label: ReactNode, halfChecked(treeCheckStrictly 时有效): string\[] } 的格式 | boolean | false | |
|
||||
| listHeight | 设置弹窗滚动高度 | number | 256 | |
|
||||
| loadData | 异步加载数据 | function(node) | - | |
|
||||
| maxTagCount | 最多显示多少个 tag | number | - | |
|
||||
| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
|
||||
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
|
||||
| multiple | 支持多选(当设置 treeCheckable 时自动变为 true) | boolean | false | |
|
||||
| placeholder | 选择框默认文字 | string | - | |
|
||||
|
@ -32,6 +32,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
listType,
|
||||
onPreview,
|
||||
onDownload,
|
||||
onChange,
|
||||
previewFile,
|
||||
disabled,
|
||||
locale: propLocale,
|
||||
@ -44,6 +45,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
children,
|
||||
style,
|
||||
itemRender,
|
||||
maxCount,
|
||||
} = props;
|
||||
|
||||
const [dragState, setDragState] = React.useState<string>('drop');
|
||||
@ -71,16 +73,22 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onChange = (info: UploadChangeParam) => {
|
||||
setFileList(info.fileList);
|
||||
const onInternalChange = (info: UploadChangeParam) => {
|
||||
let cloneList = [...info.fileList];
|
||||
|
||||
const { onChange: onChangeProp } = props;
|
||||
if (onChangeProp) {
|
||||
onChangeProp({
|
||||
...info,
|
||||
fileList: [...info.fileList],
|
||||
});
|
||||
// Cut to match count
|
||||
if (maxCount === 1) {
|
||||
cloneList = cloneList.slice(-1);
|
||||
} else if (maxCount) {
|
||||
cloneList = cloneList.slice(0, maxCount);
|
||||
}
|
||||
|
||||
setFileList(cloneList);
|
||||
|
||||
onChange?.({
|
||||
...info,
|
||||
fileList: cloneList,
|
||||
});
|
||||
};
|
||||
|
||||
const onStart = (file: RcFile) => {
|
||||
@ -96,7 +104,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
nextFileList[fileIndex] = targetItem;
|
||||
}
|
||||
|
||||
onChange({
|
||||
onInternalChange({
|
||||
file: targetItem,
|
||||
fileList: nextFileList,
|
||||
});
|
||||
@ -118,7 +126,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
targetItem.status = 'done';
|
||||
targetItem.response = response;
|
||||
targetItem.xhr = xhr;
|
||||
onChange({
|
||||
onInternalChange({
|
||||
file: { ...targetItem },
|
||||
fileList: getFileList().concat(),
|
||||
});
|
||||
@ -131,7 +139,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
return;
|
||||
}
|
||||
targetItem.percent = e.percent;
|
||||
onChange({
|
||||
onInternalChange({
|
||||
event: e,
|
||||
file: { ...targetItem },
|
||||
fileList: getFileList().concat(),
|
||||
@ -147,7 +155,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
targetItem.error = error;
|
||||
targetItem.response = response;
|
||||
targetItem.status = 'error';
|
||||
onChange({
|
||||
onInternalChange({
|
||||
file: { ...targetItem },
|
||||
fileList: getFileList().concat(),
|
||||
});
|
||||
@ -168,7 +176,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
upload.current.abort(file);
|
||||
}
|
||||
|
||||
onChange({
|
||||
onInternalChange({
|
||||
file,
|
||||
fileList: removedFileList,
|
||||
});
|
||||
@ -197,7 +205,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
||||
}
|
||||
});
|
||||
|
||||
onChange({
|
||||
onInternalChange({
|
||||
file,
|
||||
fileList: uniqueList,
|
||||
});
|
||||
|
@ -1917,6 +1917,122 @@ exports[`renders ./components/upload/demo/fileList.md correctly 1`] = `
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/upload/demo/max-count.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:24px"
|
||||
>
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
class="ant-upload ant-upload-select ant-upload-select-picture"
|
||||
>
|
||||
<span
|
||||
class="ant-upload"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept=""
|
||||
style="display:none"
|
||||
type="file"
|
||||
/>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="upload"
|
||||
class="anticon anticon-upload"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="upload"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Upload (Max: 1)
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-upload-list ant-upload-list-picture"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
class="ant-upload ant-upload-select ant-upload-select-picture"
|
||||
>
|
||||
<span
|
||||
class="ant-upload"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept=""
|
||||
multiple=""
|
||||
style="display:none"
|
||||
type="file"
|
||||
/>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="upload"
|
||||
class="anticon anticon-upload"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="upload"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Upload (Max: 3)
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-upload-list ant-upload-list-picture"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
|
||||
<span
|
||||
class="ant-upload-picture-card-wrapper"
|
||||
|
@ -9,6 +9,7 @@ import { setup, teardown } from './mock';
|
||||
import { resetWarned } from '../../_util/devWarning';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
|
||||
describe('Upload', () => {
|
||||
mountTest(Upload);
|
||||
@ -629,4 +630,96 @@ describe('Upload', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxCount', () => {
|
||||
it('replace when only 1', async () => {
|
||||
const onChange = jest.fn();
|
||||
const fileList = [
|
||||
{
|
||||
uid: 'bar',
|
||||
name: 'bar.png',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
action: 'http://upload.com',
|
||||
fileList,
|
||||
onChange,
|
||||
maxCount: 1,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload {...props}>
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
|
||||
wrapper.find('input').simulate('change', {
|
||||
target: {
|
||||
files: [
|
||||
new File(['foo'], 'foo.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
|
||||
expect(onChange.mock.calls[0][0].fileList).toHaveLength(1);
|
||||
expect(onChange.mock.calls[0][0].fileList[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
name: 'foo.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('maxCount > 1', async () => {
|
||||
const onChange = jest.fn();
|
||||
const fileList = [
|
||||
{
|
||||
uid: 'bar',
|
||||
name: 'bar.png',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
action: 'http://upload.com',
|
||||
fileList,
|
||||
onChange,
|
||||
maxCount: 2,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload {...props}>
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
|
||||
wrapper.find('input').simulate('change', {
|
||||
target: {
|
||||
files: [
|
||||
new File(['foo'], 'foo.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
new File(['invisible'], 'invisible.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(20);
|
||||
|
||||
expect(onChange.mock.calls[0][0].fileList).toHaveLength(2);
|
||||
expect(onChange.mock.calls[0][0].fileList).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'bar.png',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'foo.png',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
40
components/upload/demo/max-count.md
Normal file
40
components/upload/demo/max-count.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
order: 10
|
||||
title:
|
||||
zh-CN: 限制数量
|
||||
en-US: Max Count
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `maxCount` 限制上传数量。当为 `1` 时,始终用最新上传的代替当前。
|
||||
|
||||
## en-US
|
||||
|
||||
Limit files with `maxCount`. Will replace current one when `maxCount` is `1`.
|
||||
|
||||
```jsx
|
||||
import { Upload, Button, Space } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
|
||||
ReactDOM.render(
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Upload
|
||||
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||
listType="picture"
|
||||
maxCount={1}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Upload (Max: 1)</Button>
|
||||
</Upload>
|
||||
<Upload
|
||||
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||
listType="picture"
|
||||
maxCount={3}
|
||||
multiple
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Upload (Max: 3)</Button>
|
||||
</Upload>
|
||||
</Space>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -108,6 +108,8 @@ export interface UploadProps<T = any> {
|
||||
isImageUrl?: (file: UploadFile) => boolean;
|
||||
progress?: UploadListProgressProps;
|
||||
itemRender?: ItemRender<T>;
|
||||
/** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
|
||||
maxCount?: number;
|
||||
}
|
||||
|
||||
export interface UploadState<T = any> {
|
||||
|
18
package.json
18
package.json
@ -124,27 +124,27 @@
|
||||
"rc-drawer": "~4.1.0",
|
||||
"rc-dropdown": "~3.2.0",
|
||||
"rc-field-form": "~1.17.3",
|
||||
"rc-image": "~4.2.0",
|
||||
"rc-image": "~4.4.0",
|
||||
"rc-input-number": "~6.1.0",
|
||||
"rc-mentions": "~1.5.0",
|
||||
"rc-menu": "~8.10.0",
|
||||
"rc-motion": "^2.4.0",
|
||||
"rc-notification": "~4.5.2",
|
||||
"rc-pagination": "~3.1.2",
|
||||
"rc-picker": "~2.4.1",
|
||||
"rc-picker": "~2.5.1",
|
||||
"rc-progress": "~3.1.0",
|
||||
"rc-rate": "~2.9.0",
|
||||
"rc-resize-observer": "^0.2.3",
|
||||
"rc-select": "~11.5.3",
|
||||
"rc-slider": "~9.6.1",
|
||||
"rc-resize-observer": "^1.0.0",
|
||||
"rc-select": "~12.0.0",
|
||||
"rc-slider": "~9.7.1",
|
||||
"rc-steps": "~4.1.0",
|
||||
"rc-switch": "~3.2.0",
|
||||
"rc-table": "~7.11.0",
|
||||
"rc-table": "~7.12.0",
|
||||
"rc-tabs": "~11.7.0",
|
||||
"rc-textarea": "~0.3.0",
|
||||
"rc-tooltip": "~5.0.0",
|
||||
"rc-tree": "~4.1.0",
|
||||
"rc-tree-select": "~4.2.0",
|
||||
"rc-tree-select": "~4.3.0",
|
||||
"rc-upload": "~3.3.4",
|
||||
"rc-util": "^5.1.0",
|
||||
"scroll-into-view-if-needed": "^2.2.25",
|
||||
@ -242,7 +242,7 @@
|
||||
"rc-scroll-anim": "^2.5.8",
|
||||
"rc-trigger": "^5.1.2",
|
||||
"rc-tween-one": "^2.4.1",
|
||||
"rc-virtual-list": "^3.0.3",
|
||||
"rc-virtual-list": "^3.2.4",
|
||||
"react": "^17.0.1",
|
||||
"react-color": "^2.17.3",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
@ -295,7 +295,7 @@
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"maxSize": "270 kB"
|
||||
"maxSize": "275 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/antd.min.css",
|
||||
|
Loading…
Reference in New Issue
Block a user