mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 11:10:01 +08:00
merge feature into master-to-merge-feature
This commit is contained in:
commit
bbc95a12a4
@ -80,6 +80,46 @@ exports[`renders ./components/cascader/demo/change-on-select.md correctly 1`] =
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/cascader/demo/custom-dropdown.md correctly 1`] = `
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="ant-cascader-picker-label"
|
||||
/>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input ant-cascader-input "
|
||||
placeholder="Please select"
|
||||
readonly=""
|
||||
tabindex="-1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-cascader-picker-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/cascader/demo/custom-render.md correctly 1`] = `
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
@ -551,7 +591,7 @@ exports[`renders ./components/cascader/demo/size.md correctly 1`] = `
|
||||
`;
|
||||
|
||||
exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
tabindex="0"
|
||||
@ -588,10 +628,11 @@ exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>,
|
||||
<br />,
|
||||
<br />,
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
style="margin-top:1rem"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -611,6 +652,84 @@ exports[`renders ./components/cascader/demo/suffix.md correctly 1`] = `
|
||||
>
|
||||
ab
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>,
|
||||
<br />,
|
||||
<br />,
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="ant-cascader-picker-label"
|
||||
/>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input ant-cascader-input "
|
||||
placeholder="Please select"
|
||||
readonly=""
|
||||
tabindex="-1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-cascader-picker-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>,
|
||||
<br />,
|
||||
<br />,
|
||||
<span
|
||||
class="ant-cascader-picker"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="ant-cascader-picker-label"
|
||||
/>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input ant-cascader-input "
|
||||
placeholder="Please select"
|
||||
readonly=""
|
||||
tabindex="-1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-cascader-picker-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
72
components/cascader/demo/custom-dropdown.md
Normal file
72
components/cascader/demo/custom-dropdown.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
order: 12
|
||||
title:
|
||||
zh-CN: 扩展菜单
|
||||
en-US: Custom dropdown
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
使用 `dropdownRender` 对下拉菜单进行自由扩展。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize the dropdown menu via `dropdownRender`.
|
||||
|
||||
```jsx
|
||||
import { Cascader, Divider } from 'antd';
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: 'Hangzhou',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: 'West Lake',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiangsu',
|
||||
label: 'Jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: 'Nanjing',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: 'Zhong Hua Men',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function dropdownRender(menus) {
|
||||
return (
|
||||
<div>
|
||||
{menus}
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<div style={{ padding: 8 }}>The footer is not very short.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Cascader
|
||||
options={options}
|
||||
dropdownRender={dropdownRender}
|
||||
placeholder="Please select"
|
||||
/>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -2,17 +2,17 @@
|
||||
order: 11
|
||||
debug: true
|
||||
title:
|
||||
zh-CN: 后缀图标
|
||||
en-US: Suffix
|
||||
zh-CN: 自定义图标
|
||||
en-US: Custom Icons
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
省市区级联。
|
||||
通过 `suffixIcon` 自定义选择框后缀图标,通过 `expandIcon` 自定义次级菜单展开图标。
|
||||
|
||||
## en-US
|
||||
|
||||
Cascade selection box for selecting province/city/district.
|
||||
Use `suffixIcon` to customize the selection box suffix icon, and use `expandIcon` to customize the current item expand icon.
|
||||
|
||||
```jsx
|
||||
import { Cascader } from 'antd';
|
||||
@ -58,21 +58,28 @@ function onChange(value) {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<>
|
||||
<Cascader
|
||||
suffixIcon={<SmileOutlined />}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
placeholder="Please select"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Cascader suffixIcon="ab" options={options} onChange={onChange} placeholder="Please select" />
|
||||
<br />
|
||||
<br />
|
||||
<Cascader
|
||||
suffixIcon="ab"
|
||||
style={{ marginTop: '1rem' }}
|
||||
expandIcon={<SmileOutlined />}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
placeholder="Please select"
|
||||
/>
|
||||
</div>,
|
||||
<br />
|
||||
<br />
|
||||
<Cascader expandIcon="ab" options={options} onChange={onChange} placeholder="Please select" />
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
||||
|
@ -30,6 +30,7 @@ Cascade selection box.
|
||||
| disabled | whether disabled select | boolean | false | |
|
||||
| displayRender | render function of displaying selected options | `(label, selectedOptions) => ReactNode` | `label => label.join(' / ')` | |
|
||||
| expandTrigger | expand current item when click or hover, one of 'click' 'hover' | string | `click` | |
|
||||
| expandIcon | customize the current item expand icon | ReactNode | - | 4.4.0 |
|
||||
| fieldNames | custom field name for label and value and children | object | `{ label: 'label', value: 'value', children: 'children' }` | |
|
||||
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative.[example](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | () => document.body | |
|
||||
| loadData | To load option lazily, and it cannot work with `showSearch` | `(selectedOptions) => void` | - | |
|
||||
@ -44,6 +45,7 @@ Cascade selection box.
|
||||
| style | additional style | CSSProperties | - | |
|
||||
| suffixIcon | The custom suffix icon | ReactNode | - | |
|
||||
| value | selected value | string\[] \| number\[] | - | |
|
||||
| dropdownRender | Customize dropdown content | `(menus: ReactNode) => ReactNode` | - | 4.4.0 |
|
||||
| onChange | callback when finishing cascader select | `(value, selectedOptions) => void` | - | |
|
||||
| onPopupVisibleChange | callback when popup shown or hidden | `(value) => void` | - | |
|
||||
|
||||
|
@ -97,6 +97,7 @@ export interface CascaderProps {
|
||||
loadData?: (selectedOptions?: CascaderOptionType[]) => void;
|
||||
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
|
||||
expandTrigger?: CascaderExpandTrigger;
|
||||
expandIcon?: React.ReactNode;
|
||||
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
|
||||
changeOnSelect?: boolean;
|
||||
/** 浮层可见变化时回调 */
|
||||
@ -108,6 +109,7 @@ export interface CascaderProps {
|
||||
/** use this after antd@3.7.0 */
|
||||
fieldNames?: FieldNamesType;
|
||||
suffixIcon?: React.ReactNode;
|
||||
dropdownRender?: (menus: React.ReactNode) => React.ReactNode
|
||||
}
|
||||
|
||||
export interface CascaderState {
|
||||
@ -458,11 +460,14 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
|
||||
allowClear,
|
||||
showSearch = false,
|
||||
suffixIcon,
|
||||
expandIcon,
|
||||
notFoundContent,
|
||||
popupClassName,
|
||||
bordered,
|
||||
dropdownRender,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const mergedSize = customizeSize || size;
|
||||
|
||||
const { value, inputFocused } = state;
|
||||
@ -592,9 +597,11 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
|
||||
</span>
|
||||
);
|
||||
|
||||
let expandIcon = <RightOutlined />;
|
||||
if (isRtlLayout) {
|
||||
expandIcon = <LeftOutlined />;
|
||||
let expandIconNode;
|
||||
if (expandIcon) {
|
||||
expandIconNode = expandIcon;
|
||||
} else {
|
||||
expandIconNode = isRtlLayout ? <LeftOutlined /> : <RightOutlined />;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
@ -621,10 +628,11 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
|
||||
onPopupVisibleChange={this.handlePopupVisibleChange}
|
||||
onChange={this.handleChange}
|
||||
dropdownMenuColumnStyle={dropdownMenuColumnStyle}
|
||||
expandIcon={expandIcon}
|
||||
expandIcon={expandIconNode}
|
||||
loadingIcon={loadingIcon}
|
||||
popupClassName={rcCascaderPopupClassName}
|
||||
popupPlacement={this.getPopupPlacement(direction)}
|
||||
dropdownRender={dropdownRender}
|
||||
>
|
||||
{input}
|
||||
</RcCascader>
|
||||
|
@ -31,6 +31,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
|
||||
| disabled | 禁用 | boolean | false | |
|
||||
| displayRender | 选择后展示的渲染函数 | `(label, selectedOptions) => ReactNode` | `label => label.join(' / ')` | |
|
||||
| expandTrigger | 次级菜单的展开方式,可选 'click' 和 'hover' | string | `click` | |
|
||||
| expandIcon | 自定义次级菜单展开图标 | ReactNode | - | 4.4.0 |
|
||||
| fieldNames | 自定义 options 中 label name children 的字段 | object | `{ label: 'label', value: 'value', children: 'children' }` | |
|
||||
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | () => document.body | |
|
||||
| loadData | 用于动态加载选项,无法与 `showSearch` 一起使用 | `(selectedOptions) => void` | - | |
|
||||
@ -45,6 +46,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
|
||||
| style | 自定义样式 | CSSProperties | - | |
|
||||
| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |
|
||||
| value | 指定选中项 | string\[] \| number\[] | - | |
|
||||
| dropdownRender | 自定义下拉框内容 | `(menus: ReactNode) => ReactNode` | - | 4.4.0 |
|
||||
| onChange | 选择完成后的回调 | `(value, selectedOptions) => void` | - | |
|
||||
| onPopupVisibleChange | 显示/隐藏浮层的回调 | `(value) => void` | - | |
|
||||
|
||||
|
@ -23,6 +23,7 @@ export interface CollapseProps {
|
||||
prefixCls?: string;
|
||||
expandIcon?: (panelProps: PanelProps) => React.ReactNode;
|
||||
expandIconPosition?: ExpandIconPosition;
|
||||
ghost?: boolean;
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
@ -42,7 +43,7 @@ interface CollapseInterface extends React.FC<CollapseProps> {
|
||||
|
||||
const Collapse: CollapseInterface = props => {
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const { prefixCls: customizePrefixCls, className = '', bordered } = props;
|
||||
const { prefixCls: customizePrefixCls, className = '', bordered, ghost } = props;
|
||||
const prefixCls = getPrefixCls('collapse', customizePrefixCls);
|
||||
|
||||
const getIconPosition = () => {
|
||||
@ -72,6 +73,7 @@ const Collapse: CollapseInterface = props => {
|
||||
[`${prefixCls}-borderless`]: !bordered,
|
||||
[`${prefixCls}-icon-position-${iconPosition}`]: true,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
[`${prefixCls}-ghost`]: !!ghost,
|
||||
},
|
||||
className,
|
||||
);
|
||||
|
@ -718,6 +718,125 @@ exports[`renders ./components/collapse/demo/extra.md correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/collapse/demo/ghost.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-collapse ant-collapse-icon-position-left ant-collapse-ghost"
|
||||
>
|
||||
<div
|
||||
class="ant-collapse-item ant-collapse-item-active"
|
||||
>
|
||||
<div
|
||||
aria-expanded="true"
|
||||
class="ant-collapse-header"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="right"
|
||||
class="anticon anticon-right ant-collapse-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="right"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
style="-ms-transform:rotate(90deg);transform:rotate(90deg)"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
This is panel header 1
|
||||
</div>
|
||||
<div
|
||||
class="ant-collapse-content ant-collapse-content-active"
|
||||
>
|
||||
<div
|
||||
class="ant-collapse-content-box"
|
||||
>
|
||||
<p>
|
||||
|
||||
A dog is a type of domesticated animal.
|
||||
Known for its loyalty and faithfulness,
|
||||
it can be found as a welcome guest in many households across the world.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-collapse-item"
|
||||
>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
class="ant-collapse-header"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="right"
|
||||
class="anticon anticon-right ant-collapse-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="right"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
This is panel header 2
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-collapse-item"
|
||||
>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
class="ant-collapse-header"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="right"
|
||||
class="anticon anticon-right ant-collapse-arrow"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="right"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
This is panel header 3
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/collapse/demo/mix.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-collapse ant-collapse-icon-position-left"
|
||||
|
41
components/collapse/demo/ghost.md
Normal file
41
components/collapse/demo/ghost.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 幽灵折叠面板
|
||||
en-US: Ghost Collapse
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
将折叠面板的背景变成透明。
|
||||
|
||||
## en-US
|
||||
|
||||
Making collapse's background to transparent.
|
||||
|
||||
```jsx
|
||||
import { Collapse } from 'antd';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const text = `
|
||||
A dog is a type of domesticated animal.
|
||||
Known for its loyalty and faithfulness,
|
||||
it can be found as a welcome guest in many households across the world.
|
||||
`;
|
||||
|
||||
ReactDOM.render(
|
||||
<Collapse defaultActiveKey={['1']} ghost>
|
||||
<Panel header="This is panel header 1" key="1">
|
||||
<p>{text}</p>
|
||||
</Panel>
|
||||
<Panel header="This is panel header 2" key="2">
|
||||
<p>{text}</p>
|
||||
</Panel>
|
||||
<Panel header="This is panel header 3" key="3">
|
||||
<p>{text}</p>
|
||||
</Panel>
|
||||
</Collapse>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -27,6 +27,7 @@ A content area which can be collapsed and expanded.
|
||||
| expandIcon | allow to customize collapse icon | (panelProps) => ReactNode | - | |
|
||||
| expandIconPosition | Set expand icon position | `left` \| `right` | - | |
|
||||
| destroyInactivePanel | Destroy Inactive Panel | boolean | false | |
|
||||
| ghost | make the collapse borderless and its background transparent | boolean | false | 4.4.0 |
|
||||
|
||||
### Collapse.Panel
|
||||
|
||||
|
@ -28,6 +28,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/IxH16B9RD/Collapse.svg
|
||||
| expandIcon | 自定义切换图标 | (panelProps) => ReactNode | - | |
|
||||
| expandIconPosition | 设置图标位置 | `left` \| `right` | - | |
|
||||
| destroyInactivePanel | 销毁折叠隐藏的面板 | boolean | false | |
|
||||
| ghost | 使折叠面板透明且无边框 | boolean | false | 4.4.0 |
|
||||
|
||||
### Collapse.Panel
|
||||
|
||||
|
@ -125,6 +125,22 @@
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&-ghost {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
> .@{collapse-prefix-cls}-item {
|
||||
border-bottom: 0;
|
||||
> .@{collapse-prefix-cls}-content {
|
||||
background-color: transparent;
|
||||
border-top: 0;
|
||||
> .@{collapse-prefix-cls}-content-box {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& &-item-disabled > &-header {
|
||||
&,
|
||||
& > .arrow {
|
||||
|
@ -24,7 +24,9 @@ exports[`MonthPicker and WeekPicker render MonthPicker 1`] = `
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
«
|
||||
<span
|
||||
class="ant-picker-super-prev-icon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="ant-picker-header-view"
|
||||
@ -41,7 +43,9 @@ exports[`MonthPicker and WeekPicker render MonthPicker 1`] = `
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
»
|
||||
<span
|
||||
class="ant-picker-super-next-icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
@ -92,6 +92,7 @@ The following APIs are shared by DatePicker, YearPicker, MonthPicker, RangePicke
|
||||
| onChange | a callback function, can be executed when the selected time is changing | function(date: moment, dateString: string) | - | |
|
||||
| onOk | callback when click ok button | function() | - | |
|
||||
| onPanelChange | Callback function for panel changing | function(value, mode) | - | |
|
||||
| showNow | Whether to show 'Now' button on panel when `showTime` is set | boolean | - | 4.4.0 |
|
||||
|
||||
### YearPicker
|
||||
|
||||
|
@ -94,6 +94,7 @@ import 'moment/locale/zh-cn';
|
||||
| onChange | 时间发生变化的回调 | function(date: moment, dateString: string) | - | |
|
||||
| onOk | 点击确定按钮的回调 | function() | - | |
|
||||
| onPanelChange | 日期面板变化时的回调 | function(value, mode) | - | |
|
||||
| showNow | 当设定了 `showTime` 的时候,面板是否显示“此刻”按钮 | boolean | - | 4.4.0 |
|
||||
|
||||
### YearPicker
|
||||
|
||||
|
@ -125,4 +125,13 @@ describe('Drawer', () => {
|
||||
);
|
||||
expect(wrapper2.find('button.forceRender').length).toBe(1);
|
||||
});
|
||||
|
||||
it('support closeIcon', () => {
|
||||
const wrapper = render(
|
||||
<Drawer visible closable closeIcon={<span>close</span>} width={400} getContainer={false}>
|
||||
Here is content of Drawer
|
||||
</Drawer>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -82,7 +82,7 @@ describe('Drawer', () => {
|
||||
expect(wrapper.instance().state.visible).toBe(true);
|
||||
});
|
||||
|
||||
it('destroyOnClose is true onClose', () => {
|
||||
it('dom should be removed after close when destroyOnClose is true', () => {
|
||||
const wrapper = mount(<DrawerEventTester destroyOnClose />);
|
||||
wrapper.find('button.ant-btn').simulate('click');
|
||||
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
|
||||
@ -94,6 +94,18 @@ describe('Drawer', () => {
|
||||
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('dom should be existed after close when destroyOnClose is false', () => {
|
||||
const wrapper = mount(<DrawerEventTester />);
|
||||
wrapper.find('button.ant-btn').simulate('click');
|
||||
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
|
||||
|
||||
wrapper.setState({
|
||||
visible: false,
|
||||
});
|
||||
wrapper.find('.ant-drawer-wrapper-body').simulate('transitionend');
|
||||
expect(wrapper.find('.ant-drawer-wrapper-body').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('no mask and no closable', () => {
|
||||
const wrapper = mount(<DrawerEventTester destroyOnClose />);
|
||||
|
||||
|
@ -558,3 +558,49 @@ exports[`Drawer style/drawerStyle/headerStyle/bodyStyle should work 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Drawer support closeIcon 1`] = `
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
class="ant-drawer ant-drawer-right"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-drawer-mask"
|
||||
/>
|
||||
<div
|
||||
class="ant-drawer-content-wrapper"
|
||||
style="transform:translateX(100%);-ms-transform:translateX(100%);width:400px"
|
||||
>
|
||||
<div
|
||||
class="ant-drawer-content"
|
||||
>
|
||||
<div
|
||||
class="ant-drawer-wrapper-body"
|
||||
>
|
||||
<div
|
||||
class="ant-drawer-header-no-title"
|
||||
>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="ant-drawer-close"
|
||||
style="--scroll-bar:0px"
|
||||
>
|
||||
<span>
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-drawer-body"
|
||||
>
|
||||
Here is content of Drawer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -21,6 +21,7 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
|
||||
| Props | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not. | boolean | true |
|
||||
| closeIcon | custom close icon | ReactNode | `<CloseOutlined />` |
|
||||
| destroyOnClose | Whether to unmount child components on closing drawer or not. | boolean | false |
|
||||
| forceRender | Prerender Drawer component forcely | boolean | false |
|
||||
| getContainer | Return the mounted node for Drawer. | HTMLElement \| `() => HTMLElement` \| Selectors \| false | 'body' |
|
||||
|
@ -21,6 +21,7 @@ const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
|
||||
type placementType = typeof PlacementTypes[number];
|
||||
export interface DrawerProps {
|
||||
closable?: boolean;
|
||||
closeIcon?: React.ReactNode;
|
||||
destroyOnClose?: boolean;
|
||||
forceRender?: boolean;
|
||||
getContainer?: string | HTMLElement | getContainerFunc | false;
|
||||
@ -195,7 +196,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
|
||||
}
|
||||
|
||||
renderCloseIcon() {
|
||||
const { closable, prefixCls, onClose } = this.props;
|
||||
const { closable, closeIcon = <CloseOutlined />, prefixCls, onClose } = this.props;
|
||||
return (
|
||||
closable && (
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
@ -209,7 +210,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<CloseOutlined />
|
||||
{closeIcon}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
@ -283,6 +284,7 @@ class Drawer extends React.Component<DrawerProps & ConfigConsumerProps, IDrawerS
|
||||
'zIndex',
|
||||
'style',
|
||||
'closable',
|
||||
'closeIcon',
|
||||
'destroyOnClose',
|
||||
'drawerStyle',
|
||||
'headerStyle',
|
||||
|
@ -20,6 +20,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| closable | 是否显示右上角的关闭按钮 | boolean | true |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | `<CloseOutlined />` |
|
||||
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false |
|
||||
| forceRender | 预渲染 Drawer 内元素 | boolean | false |
|
||||
| getContainer | 指定 Drawer 挂载的 HTML 节点, false 为挂载在当前 dom | HTMLElement \| `() => HTMLElement` \| Selectors \| false | 'body' |
|
||||
|
@ -1,5 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./components/dropdown/demo/arrow.md correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
bottomLeft
|
||||
</span>
|
||||
</button>,
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
bottomCenter
|
||||
</span>
|
||||
</button>,
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
bottomRight
|
||||
</span>
|
||||
</button>,
|
||||
<br />,
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
topLeft
|
||||
</span>
|
||||
</button>,
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
topCenter
|
||||
</span>
|
||||
</button>,
|
||||
<button
|
||||
class="ant-btn ant-dropdown-trigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
topRight
|
||||
</span>
|
||||
</button>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/basic.md correctly 1`] = `
|
||||
<a
|
||||
class="ant-dropdown-link ant-dropdown-trigger"
|
||||
|
75
components/dropdown/demo/arrow.md
Normal file
75
components/dropdown/demo/arrow.md
Normal file
@ -0,0 +1,75 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 箭头
|
||||
en-US: Arrow
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以展示一个箭头。
|
||||
|
||||
## en-US
|
||||
|
||||
You could display an arrow.
|
||||
|
||||
```jsx
|
||||
import { Menu, Dropdown, Button } from 'antd';
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item>
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
|
||||
1st menu item
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
|
||||
2nd menu item
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
|
||||
3rd menu item
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<Dropdown overlay={menu} placement="bottomLeft" arrow>
|
||||
<Button>bottomLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottomCenter" arrow>
|
||||
<Button>bottomCenter</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottomRight" arrow>
|
||||
<Button>bottomRight</Button>
|
||||
</Dropdown>
|
||||
<br />
|
||||
<Dropdown overlay={menu} placement="topLeft" arrow>
|
||||
<Button>topLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="topCenter" arrow>
|
||||
<Button>topCenter</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="topRight" arrow>
|
||||
<Button>topRight</Button>
|
||||
</Dropdown>
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
||||
|
||||
```css
|
||||
#components-dropdown-demo-arrow .ant-btn {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ant-row-rtl #components-dropdown-demo-arrow .ant-btn {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
```
|
@ -35,6 +35,7 @@ type Align = {
|
||||
};
|
||||
|
||||
export interface DropDownProps {
|
||||
arrow?: boolean;
|
||||
trigger?: ('click' | 'hover' | 'contextMenu')[];
|
||||
overlay: React.ReactElement | OverlayFunc;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
@ -130,6 +131,7 @@ const Dropdown: DropdownInterface = props => {
|
||||
};
|
||||
|
||||
const {
|
||||
arrow,
|
||||
prefixCls: customizePrefixCls,
|
||||
children,
|
||||
trigger,
|
||||
@ -160,6 +162,7 @@ const Dropdown: DropdownInterface = props => {
|
||||
|
||||
return (
|
||||
<RcDropdown
|
||||
arrow={arrow}
|
||||
alignPoint={alignPoint}
|
||||
{...props}
|
||||
overlayClassName={overlayClassNameCustomized}
|
||||
|
@ -17,7 +17,8 @@ When there are more than a few options to choose from, you can wrap them in a `D
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| disabled | Whether the dropdown menu is disabled | boolean | false | |
|
||||
| arrow | Whether the dropdown arrow should be visible | boolean | false | |
|
||||
| disabled | Whether the dropdown menu is disabled | boolean | - | |
|
||||
| getPopupContainer | To set the container of the dropdown menu. The default is to create a `div` element in `body`, but you can reset it to the scrolling area and make a relative reposition. [Example on CodePen](https://codepen.io/afc163/pen/zEjNOy?editors=0010). | Function(triggerNode) | `() => document.body` | |
|
||||
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| overlayClassName | Class name of the dropdown root element | string | - | |
|
||||
|
@ -21,7 +21,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| disabled | 菜单是否禁用 | boolean | false | |
|
||||
| arrow | 下拉框箭头是否显示 | boolean | false | |
|
||||
| disabled | 菜单是否禁用 | boolean | - | |
|
||||
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | `() => document.body` | |
|
||||
| overlay | 菜单 | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| overlayClassName | 下拉根元素的类名称 | string | - | |
|
||||
|
@ -47,6 +47,76 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Offset the popover to account for the dropdown arrow
|
||||
&-show-arrow&-placement-topCenter,
|
||||
&-show-arrow&-placement-topLeft,
|
||||
&-show-arrow&-placement-topRight {
|
||||
padding-bottom: @popover-distance;
|
||||
}
|
||||
|
||||
&-show-arrow&-placement-bottomCenter,
|
||||
&-show-arrow&-placement-bottomLeft,
|
||||
&-show-arrow&-placement-bottomRight {
|
||||
padding-top: @popover-distance;
|
||||
}
|
||||
|
||||
// Arrows
|
||||
// .popover-arrow is outer, .popover-arrow:after is inner
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
z-index: 1; // lift it up so the menu wouldn't cask shadow on it
|
||||
display: block;
|
||||
width: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
|
||||
height: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
|
||||
background: transparent;
|
||||
border-style: solid;
|
||||
border-width: sqrt(@popover-arrow-width * @popover-arrow-width * 2) / 2;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&-placement-topCenter > &-arrow,
|
||||
&-placement-topLeft > &-arrow,
|
||||
&-placement-topRight > &-arrow {
|
||||
bottom: @popover-distance - @popover-arrow-width + 2.2px;
|
||||
border-top-color: transparent;
|
||||
border-right-color: @popover-bg;
|
||||
border-bottom-color: @popover-bg;
|
||||
border-left-color: transparent;
|
||||
box-shadow: 3px 3px 7px fade(@black, 7%);
|
||||
}
|
||||
&-placement-topCenter > &-arrow {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
&-placement-topLeft > &-arrow {
|
||||
left: 16px;
|
||||
}
|
||||
&-placement-topRight > &-arrow {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
&-placement-bottomCenter > &-arrow,
|
||||
&-placement-bottomLeft > &-arrow,
|
||||
&-placement-bottomRight > &-arrow {
|
||||
top: @popover-distance - @popover-arrow-width + 2px;
|
||||
border-top-color: @popover-bg;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: @popover-bg;
|
||||
box-shadow: -2px -2px 5px fade(@black, 6%);
|
||||
}
|
||||
&-placement-bottomCenter > &-arrow {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
&-placement-bottomLeft > &-arrow {
|
||||
left: 16px;
|
||||
}
|
||||
&-placement-bottomRight > &-arrow {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import FieldForm, { List } from 'rc-field-form';
|
||||
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
|
||||
@ -8,7 +7,7 @@ import { ColProps } from '../grid/col';
|
||||
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
|
||||
import { FormContext } from './context';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { useForm, FormInstance } from './util';
|
||||
import useForm, { FormInstance } from './hooks/useForm';
|
||||
import SizeContext, { SizeType, SizeContextProvider } from '../config-provider/SizeContext';
|
||||
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
@ -31,21 +30,24 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
const contextSize = React.useContext(SizeContext);
|
||||
const { getPrefixCls, direction }: ConfigConsumerProps = React.useContext(ConfigContext);
|
||||
|
||||
const { name } = props;
|
||||
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className = '',
|
||||
size = contextSize,
|
||||
form,
|
||||
colon,
|
||||
name,
|
||||
labelAlign,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
prefixCls: customizePrefixCls,
|
||||
hideRequiredMark,
|
||||
className = '',
|
||||
layout = 'horizontal',
|
||||
size = contextSize,
|
||||
scrollToFirstError,
|
||||
onFinishFailed,
|
||||
...restFormProps
|
||||
} = props;
|
||||
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
|
||||
const formClassName = classNames(
|
||||
@ -59,20 +61,9 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
className,
|
||||
);
|
||||
|
||||
const formProps = omit(props, [
|
||||
'prefixCls',
|
||||
'className',
|
||||
'layout',
|
||||
'hideRequiredMark',
|
||||
'wrapperCol',
|
||||
'labelAlign',
|
||||
'labelCol',
|
||||
'colon',
|
||||
'scrollToFirstError',
|
||||
]);
|
||||
|
||||
const [wrapForm] = useForm(form);
|
||||
wrapForm.__INTERNAL__.name = name;
|
||||
const { __INTERNAL__ } = wrapForm;
|
||||
__INTERNAL__.name = name;
|
||||
|
||||
const formContextValue = React.useMemo(
|
||||
() => ({
|
||||
@ -82,6 +73,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
wrapperCol,
|
||||
vertical: layout === 'vertical',
|
||||
colon,
|
||||
itemRef: __INTERNAL__.itemRef,
|
||||
}),
|
||||
[name, labelAlign, labelCol, wrapperCol, layout, colon],
|
||||
);
|
||||
@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
|
||||
return (
|
||||
<SizeContextProvider size={size}>
|
||||
<FormContext.Provider
|
||||
value={formContextValue}
|
||||
>
|
||||
<FormContext.Provider value={formContextValue}>
|
||||
<FieldForm
|
||||
id={name}
|
||||
{...formProps}
|
||||
{...restFormProps}
|
||||
onFinishFailed={onInternalFinishFailed}
|
||||
form={wrapForm}
|
||||
className={formClassName}
|
||||
|
@ -5,6 +5,7 @@ import { Field, FormInstance } from 'rc-field-form';
|
||||
import { FieldProps } from 'rc-field-form/lib/Field';
|
||||
import FieldContext from 'rc-field-form/lib/FieldContext';
|
||||
import { Meta, NamePath } from 'rc-field-form/lib/interface';
|
||||
import { supportRef } from 'rc-util/lib/ref';
|
||||
import omit from 'omit.js';
|
||||
import Row from '../grid/row';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
@ -13,8 +14,10 @@ import devWarning from '../_util/devWarning';
|
||||
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
|
||||
import FormItemInput, { FormItemInputProps } from './FormItemInput';
|
||||
import { FormContext, FormItemContext } from './context';
|
||||
import { toArray, getFieldId, useFrameState } from './util';
|
||||
import { toArray, getFieldId } from './util';
|
||||
import { cloneElement, isValidElement } from '../_util/reactNode';
|
||||
import useFrameState from './hooks/useFrameState';
|
||||
import useItemRef from './hooks/useItemRef';
|
||||
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = typeof ValidateStatuses[number];
|
||||
@ -81,7 +84,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
} = props;
|
||||
const destroyRef = React.useRef(false);
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const formContext = React.useContext(FormContext);
|
||||
const { name: formName } = React.useContext(FormContext);
|
||||
const { updateItemErrors } = React.useContext(FormItemContext);
|
||||
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
||||
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
|
||||
@ -97,7 +100,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
const { name: formName } = formContext;
|
||||
const hasName = hasValidName(name);
|
||||
|
||||
// Cache Field NamePath
|
||||
@ -126,6 +128,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== Children Ref =====================
|
||||
const getItemRef = useItemRef();
|
||||
|
||||
function renderLayout(
|
||||
baseChildren: React.ReactNode,
|
||||
fieldId?: string,
|
||||
@ -323,6 +328,10 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
childProps.id = fieldId;
|
||||
}
|
||||
|
||||
if (supportRef(children)) {
|
||||
childProps.ref = getItemRef(mergedName, children);
|
||||
}
|
||||
|
||||
// We should keep user origin event handler
|
||||
const triggers = new Set<string>([
|
||||
...toArray(trigger),
|
||||
|
@ -10,7 +10,7 @@ import CSSMotion from 'rc-animate/lib/CSSMotion';
|
||||
import Col, { ColProps } from '../grid/col';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import { FormContext } from './context';
|
||||
import { useCacheErrors } from './util';
|
||||
import useCacheErrors from './hooks/useCacheErrors';
|
||||
|
||||
interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
|
@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/ref-item.md correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="test"
|
||||
title="test"
|
||||
>
|
||||
test
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
id="test"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
id="list_0"
|
||||
type="text"
|
||||
value="light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="ant-btn ant-btn-button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus Form.Item
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Focus Form.List
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
|
91
components/form/__tests__/ref.test.tsx
Normal file
91
components/form/__tests__/ref.test.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
/* eslint-disable react/jsx-key */
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Form from '..';
|
||||
import Input from '../../input';
|
||||
import Button from '../../button';
|
||||
|
||||
describe('Form.Ref', () => {
|
||||
const Test = ({
|
||||
onRef,
|
||||
show,
|
||||
}: {
|
||||
onRef: (node: React.ReactElement, originRef: React.RefObject<any>) => void;
|
||||
show?: boolean;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const removeRef = React.useRef<any>();
|
||||
const testRef = React.useRef<any>();
|
||||
const listRef = React.useRef<any>();
|
||||
|
||||
return (
|
||||
<Form form={form} initialValues={{ list: ['light'] }}>
|
||||
{show && (
|
||||
<Form.Item name="remove" label="remove">
|
||||
<Input ref={removeRef} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="test" label="test">
|
||||
<Input ref={testRef} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name="list">
|
||||
{fields =>
|
||||
fields.map(field => (
|
||||
<Form.Item {...field}>
|
||||
<Input ref={listRef} />
|
||||
</Form.Item>
|
||||
))
|
||||
}
|
||||
</Form.List>
|
||||
|
||||
<Button
|
||||
className="ref-item"
|
||||
onClick={() => {
|
||||
onRef(form.getFieldInstance('test'), testRef.current);
|
||||
}}
|
||||
>
|
||||
Form.Item
|
||||
</Button>
|
||||
<Button
|
||||
className="ref-list"
|
||||
onClick={() => {
|
||||
onRef(form.getFieldInstance(['list', 0]), listRef.current);
|
||||
}}
|
||||
>
|
||||
Form.List
|
||||
</Button>
|
||||
<Button
|
||||
className="ref-remove"
|
||||
onClick={() => {
|
||||
onRef(form.getFieldInstance('remove'), removeRef.current);
|
||||
}}
|
||||
>
|
||||
Removed
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
it('should ref work', () => {
|
||||
const onRef = jest.fn();
|
||||
const wrapper = mount(<Test onRef={onRef} show />);
|
||||
|
||||
wrapper.find('.ref-item').last().simulate('click');
|
||||
expect(onRef).toHaveBeenCalled();
|
||||
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
|
||||
|
||||
onRef.mockReset();
|
||||
wrapper.find('.ref-list').last().simulate('click');
|
||||
expect(onRef).toHaveBeenCalled();
|
||||
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
|
||||
|
||||
onRef.mockReset();
|
||||
wrapper.setProps({ show: false });
|
||||
wrapper.update();
|
||||
wrapper.find('.ref-remove').last().simulate('click');
|
||||
expect(onRef).toHaveBeenCalledWith(undefined, null);
|
||||
});
|
||||
});
|
@ -16,11 +16,13 @@ export interface FormContextProps {
|
||||
labelAlign?: FormLabelAlign;
|
||||
labelCol?: ColProps;
|
||||
wrapperCol?: ColProps;
|
||||
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
|
||||
}
|
||||
|
||||
export const FormContext = React.createContext<FormContextProps>({
|
||||
labelAlign: 'right',
|
||||
vertical: false,
|
||||
itemRef: (() => {}) as any,
|
||||
});
|
||||
|
||||
/**
|
||||
|
61
components/form/demo/ref-item.md
Normal file
61
components/form/demo/ref-item.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
order: 999999
|
||||
title:
|
||||
zh-CN: 引用字段
|
||||
en-US: Ref item
|
||||
debug: true
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
请优先使用 `ref`!
|
||||
|
||||
## en-US
|
||||
|
||||
Use `ref` first!
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { Button, Form, Input } from 'antd';
|
||||
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
const ref = React.useRef();
|
||||
|
||||
return (
|
||||
<Form form={form} initialValues={{ list: ['light'] }}>
|
||||
<Form.Item name="test" label="test">
|
||||
<Input ref={ref} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name="list">
|
||||
{fields =>
|
||||
fields.map(field => (
|
||||
<Form.Item key={field.key} {...field}>
|
||||
<Input ref={ref} />
|
||||
</Form.Item>
|
||||
))
|
||||
}
|
||||
</Form.List>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
form.getFieldInstance('test').focus();
|
||||
}}
|
||||
>
|
||||
Focus Form.Item
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.getFieldInstance(['list', 0]).focus();
|
||||
}}
|
||||
>
|
||||
Focus Form.List
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
48
components/form/hooks/useCacheErrors.ts
Normal file
48
components/form/hooks/useCacheErrors.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Always debounce error to avoid [error -> null -> error] blink
|
||||
*/
|
||||
export default function useCacheErrors(
|
||||
errors: React.ReactNode[],
|
||||
changeTrigger: (visible: boolean) => void,
|
||||
directly: boolean,
|
||||
): [boolean, React.ReactNode[]] {
|
||||
const cacheRef = React.useRef({
|
||||
errors,
|
||||
visible: !!errors.length,
|
||||
});
|
||||
|
||||
const [, forceUpdate] = React.useState({});
|
||||
|
||||
const update = () => {
|
||||
const prevVisible = cacheRef.current.visible;
|
||||
const newVisible = !!errors.length;
|
||||
|
||||
const prevErrors = cacheRef.current.errors;
|
||||
cacheRef.current.errors = errors;
|
||||
cacheRef.current.visible = newVisible;
|
||||
|
||||
if (prevVisible !== newVisible) {
|
||||
changeTrigger(newVisible);
|
||||
} else if (
|
||||
prevErrors.length !== errors.length ||
|
||||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
|
||||
) {
|
||||
forceUpdate({});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!directly) {
|
||||
const timeout = setTimeout(update, 10);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
if (directly) {
|
||||
update();
|
||||
}
|
||||
|
||||
return [cacheRef.current.visible, cacheRef.current.errors];
|
||||
}
|
64
components/form/hooks/useForm.ts
Normal file
64
components/form/hooks/useForm.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { ScrollOptions, NamePath, InternalNamePath } from '../interface';
|
||||
import { toArray, getFieldId } from '../util';
|
||||
|
||||
export interface FormInstance extends RcFormInstance {
|
||||
scrollToField: (name: NamePath, options?: ScrollOptions) => void;
|
||||
/** This is an internal usage. Do not use in your prod */
|
||||
__INTERNAL__: {
|
||||
/** No! Do not use this in your code! */
|
||||
name?: string;
|
||||
/** No! Do not use this in your code! */
|
||||
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => void;
|
||||
};
|
||||
getFieldInstance: (name: NamePath) => any;
|
||||
}
|
||||
|
||||
function toNamePathStr(name: NamePath) {
|
||||
const namePath = toArray(name);
|
||||
return namePath.join('_');
|
||||
}
|
||||
|
||||
export default function useForm(form?: FormInstance): [FormInstance] {
|
||||
const [rcForm] = useRcForm();
|
||||
const itemsRef = useRef<Record<string, React.ReactElement>>({});
|
||||
|
||||
const wrapForm: FormInstance = useMemo(
|
||||
() =>
|
||||
form || {
|
||||
...rcForm,
|
||||
__INTERNAL__: {
|
||||
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => {
|
||||
const namePathStr = toNamePathStr(name);
|
||||
if (node) {
|
||||
itemsRef.current[namePathStr] = node;
|
||||
} else {
|
||||
delete itemsRef.current[namePathStr];
|
||||
}
|
||||
},
|
||||
},
|
||||
scrollToField: (name: string, options: ScrollOptions = {}) => {
|
||||
const namePath = toArray(name);
|
||||
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
|
||||
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
|
||||
|
||||
if (node) {
|
||||
scrollIntoView(node, {
|
||||
scrollMode: 'if-needed',
|
||||
block: 'nearest',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
},
|
||||
getFieldInstance: (name: string) => {
|
||||
const namePathStr = toNamePathStr(name);
|
||||
return itemsRef.current[namePathStr];
|
||||
},
|
||||
},
|
||||
[form, rcForm],
|
||||
);
|
||||
|
||||
return [wrapForm];
|
||||
}
|
48
components/form/hooks/useFrameState.ts
Normal file
48
components/form/hooks/useFrameState.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { useRef } from 'react';
|
||||
import raf from 'raf';
|
||||
|
||||
type Updater<ValueType> = (prev?: ValueType) => ValueType;
|
||||
|
||||
export default function useFrameState<ValueType>(
|
||||
defaultValue: ValueType,
|
||||
): [ValueType, (updater: Updater<ValueType>) => void] {
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
const frameRef = useRef<number | null>(null);
|
||||
const batchRef = useRef<Updater<ValueType>[]>([]);
|
||||
const destroyRef = useRef(false);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
destroyRef.current = true;
|
||||
raf.cancel(frameRef.current!);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
function setFrameValue(updater: Updater<ValueType>) {
|
||||
if (destroyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameRef.current === null) {
|
||||
batchRef.current = [];
|
||||
frameRef.current = raf(() => {
|
||||
frameRef.current = null;
|
||||
setValue(prevValue => {
|
||||
let current = prevValue;
|
||||
|
||||
batchRef.current.forEach(func => {
|
||||
current = func(current);
|
||||
});
|
||||
|
||||
return current;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
batchRef.current.push(updater);
|
||||
}
|
||||
|
||||
return [value, setFrameValue];
|
||||
}
|
28
components/form/hooks/useItemRef.ts
Normal file
28
components/form/hooks/useItemRef.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import { FormContext } from '../context';
|
||||
import { InternalNamePath } from '../interface';
|
||||
|
||||
export default function useItemRef() {
|
||||
const { itemRef } = React.useContext(FormContext);
|
||||
const cacheRef = React.useRef<{
|
||||
name?: string;
|
||||
originRef?: React.Ref<any>;
|
||||
ref?: React.Ref<any>;
|
||||
}>({});
|
||||
|
||||
function getRef(name: InternalNamePath, children: any) {
|
||||
const childrenRef: React.Ref<React.ReactElement> =
|
||||
children && typeof children === 'object' && children.ref;
|
||||
const nameStr = name.join('_');
|
||||
if (cacheRef.current.name !== nameStr || cacheRef.current.originRef !== childrenRef) {
|
||||
cacheRef.current.name = nameStr;
|
||||
cacheRef.current.originRef = childrenRef;
|
||||
cacheRef.current.ref = composeRef(itemRef(name), childrenRef);
|
||||
}
|
||||
|
||||
return cacheRef.current.ref;
|
||||
}
|
||||
|
||||
return getRef;
|
||||
}
|
@ -185,21 +185,22 @@ Provide linkage between forms. If a sub form with `name` prop update, it will au
|
||||
|
||||
### FormInstance
|
||||
|
||||
| Name | Description | Type |
|
||||
| --- | --- | --- |
|
||||
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any |
|
||||
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
|
||||
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] |
|
||||
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
|
||||
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean |
|
||||
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
|
||||
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean |
|
||||
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
|
||||
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
|
||||
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void |
|
||||
| setFieldsValue | Set fields value | (values) => void |
|
||||
| submit | Submit the form. It's same as click `submit` button | () => void |
|
||||
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise |
|
||||
| Name | Description | Type | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| getFieldInstance | Get field instance | (name: [NamePath](#NamePath)) => any | 4.4.0 |
|
||||
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any | |
|
||||
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
|
||||
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] | |
|
||||
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
|
||||
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
|
||||
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
|
||||
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
|
||||
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void | |
|
||||
| setFieldsValue | Set fields value | (values) => void | |
|
||||
| submit | Submit the form. It's same as click `submit` button | () => void | |
|
||||
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise | |
|
||||
|
||||
#### validateFields return sample
|
||||
|
||||
|
@ -186,21 +186,22 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
|
||||
|
||||
### FormInstance
|
||||
|
||||
| 名称 | 说明 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any |
|
||||
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
|
||||
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] |
|
||||
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
|
||||
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean |
|
||||
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
|
||||
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
|
||||
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
|
||||
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
|
||||
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void |
|
||||
| setFieldsValue | 设置表单的值 | (values) => void |
|
||||
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |
|
||||
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise |
|
||||
| 名称 | 说明 | 类型 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| getFieldInstance | 获取对应字段示例 | (name: [NamePath](#NamePath)) => any | 4.4.0 |
|
||||
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any | |
|
||||
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
|
||||
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] | |
|
||||
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
|
||||
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
|
||||
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
|
||||
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
|
||||
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void | |
|
||||
| setFieldsValue | 设置表单的值 | (values) => void | |
|
||||
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | |
|
||||
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise | |
|
||||
|
||||
#### validateFields 返回示例
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
export { Options as ScrollOptions } from 'scroll-into-view-if-needed';
|
||||
export type FormLabelAlign = 'left' | 'right';
|
||||
export { Store, StoreValue } from 'rc-field-form/lib/interface';
|
||||
export { Store, StoreValue, NamePath, InternalNamePath } from 'rc-field-form/lib/interface';
|
||||
|
@ -1,61 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import raf from 'raf';
|
||||
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { ScrollOptions } from './interface';
|
||||
|
||||
type InternalNamePath = (string | number)[];
|
||||
|
||||
/**
|
||||
* Always debounce error to avoid [error -> null -> error] blink
|
||||
*/
|
||||
export function useCacheErrors(
|
||||
errors: React.ReactNode[],
|
||||
changeTrigger: (visible: boolean) => void,
|
||||
directly: boolean,
|
||||
): [boolean, React.ReactNode[]] {
|
||||
const cacheRef = React.useRef({
|
||||
errors,
|
||||
visible: !!errors.length,
|
||||
});
|
||||
|
||||
const [, forceUpdate] = React.useState({});
|
||||
|
||||
const update = (newErrors: React.ReactNode[]) => {
|
||||
const prevVisible = cacheRef.current.visible;
|
||||
const newVisible = !!newErrors.length;
|
||||
|
||||
const prevErrors = cacheRef.current.errors;
|
||||
cacheRef.current.errors = newErrors;
|
||||
cacheRef.current.visible = newVisible;
|
||||
|
||||
if (prevVisible !== newVisible) {
|
||||
changeTrigger(newVisible);
|
||||
} else if (
|
||||
prevErrors.length !== newErrors.length ||
|
||||
prevErrors.some((prevErr, index) => prevErr !== newErrors[index])
|
||||
) {
|
||||
forceUpdate({});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!directly) {
|
||||
const timeout = setTimeout(() => {
|
||||
update(errors);
|
||||
}, 10);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
if (directly) {
|
||||
update(errors);
|
||||
}
|
||||
|
||||
return [cacheRef.current.visible, cacheRef.current.errors];
|
||||
}
|
||||
import { InternalNamePath } from './interface';
|
||||
|
||||
export function toArray<T>(candidate?: T | T[] | false): T[] {
|
||||
if (candidate === undefined || candidate === false) return [];
|
||||
@ -69,83 +12,3 @@ export function getFieldId(namePath: InternalNamePath, formName?: string): strin
|
||||
const mergedId = namePath.join('_');
|
||||
return formName ? `${formName}_${mergedId}` : mergedId;
|
||||
}
|
||||
|
||||
export interface FormInstance extends RcFormInstance {
|
||||
scrollToField: (name: string | number | InternalNamePath, options?: ScrollOptions) => void;
|
||||
__INTERNAL__: {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useForm(form?: FormInstance): [FormInstance] {
|
||||
const [rcForm] = useRcForm();
|
||||
|
||||
const wrapForm: FormInstance = React.useMemo(
|
||||
() =>
|
||||
form || {
|
||||
...rcForm,
|
||||
__INTERNAL__: {},
|
||||
scrollToField: (name: string, options: ScrollOptions = {}) => {
|
||||
const namePath = toArray(name);
|
||||
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
|
||||
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
|
||||
|
||||
if (node) {
|
||||
scrollIntoView(node, {
|
||||
scrollMode: 'if-needed',
|
||||
block: 'nearest',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
[form, rcForm],
|
||||
);
|
||||
|
||||
return [wrapForm];
|
||||
}
|
||||
|
||||
type Updater<ValueType> = (prev?: ValueType) => ValueType;
|
||||
|
||||
export function useFrameState<ValueType>(
|
||||
defaultValue: ValueType,
|
||||
): [ValueType, (updater: Updater<ValueType>) => void] {
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
const frameRef = React.useRef<number | null>(null);
|
||||
const batchRef = React.useRef<Updater<ValueType>[]>([]);
|
||||
const destroyRef = React.useRef(false);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
destroyRef.current = true;
|
||||
raf.cancel(frameRef.current!);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
function setFrameValue(updater: Updater<ValueType>) {
|
||||
if (destroyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameRef.current === null) {
|
||||
batchRef.current = [];
|
||||
frameRef.current = raf(() => {
|
||||
frameRef.current = null;
|
||||
setValue(prevValue => {
|
||||
let current = prevValue;
|
||||
|
||||
batchRef.current.forEach(func => {
|
||||
current = func(current);
|
||||
});
|
||||
|
||||
return current;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
batchRef.current.push(updater);
|
||||
}
|
||||
|
||||
return [value, setFrameValue];
|
||||
}
|
||||
|
@ -1,158 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import calculateNodeHeight from './calculateNodeHeight';
|
||||
import raf from '../_util/raf';
|
||||
import { TextAreaProps } from './TextArea';
|
||||
|
||||
const RESIZE_STATUS_NONE = 0;
|
||||
const RESIZE_STATUS_RESIZING = 1;
|
||||
const RESIZE_STATUS_RESIZED = 2;
|
||||
|
||||
export interface AutoSizeType {
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
}
|
||||
|
||||
export interface TextAreaState {
|
||||
textareaStyles?: React.CSSProperties;
|
||||
/** We need add process style to disable scroll first and then add back to avoid unexpected scrollbar */
|
||||
resizeStatus?:
|
||||
| typeof RESIZE_STATUS_NONE
|
||||
| typeof RESIZE_STATUS_RESIZING
|
||||
| typeof RESIZE_STATUS_RESIZED;
|
||||
}
|
||||
|
||||
class ResizableTextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
nextFrameActionId: number;
|
||||
|
||||
resizeFrameId: number;
|
||||
|
||||
constructor(props: TextAreaProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
textareaStyles: {},
|
||||
resizeStatus: RESIZE_STATUS_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
textArea: HTMLTextAreaElement;
|
||||
|
||||
saveTextArea = (textArea: HTMLTextAreaElement) => {
|
||||
this.textArea = textArea;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.resizeTextarea();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TextAreaProps) {
|
||||
// Re-render with the new content then recalculate the height as required.
|
||||
if (prevProps.value !== this.props.value) {
|
||||
this.resizeTextarea();
|
||||
}
|
||||
}
|
||||
|
||||
handleResize = (size: { width: number; height: number }) => {
|
||||
const { resizeStatus } = this.state;
|
||||
const { autoSize, onResize } = this.props;
|
||||
|
||||
if (resizeStatus !== RESIZE_STATUS_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof onResize === 'function') {
|
||||
onResize(size);
|
||||
}
|
||||
if (autoSize) {
|
||||
this.resizeOnNextFrame();
|
||||
}
|
||||
};
|
||||
|
||||
resizeOnNextFrame = () => {
|
||||
raf.cancel(this.nextFrameActionId);
|
||||
this.nextFrameActionId = raf(this.resizeTextarea);
|
||||
};
|
||||
|
||||
resizeTextarea = () => {
|
||||
const { autoSize } = this.props;
|
||||
if (!autoSize || !this.textArea) {
|
||||
return;
|
||||
}
|
||||
const { minRows, maxRows } = autoSize as AutoSizeType;
|
||||
const textareaStyles = calculateNodeHeight(this.textArea, false, minRows, maxRows);
|
||||
this.setState({ textareaStyles, resizeStatus: RESIZE_STATUS_RESIZING }, () => {
|
||||
raf.cancel(this.resizeFrameId);
|
||||
this.resizeFrameId = raf(() => {
|
||||
this.setState({ resizeStatus: RESIZE_STATUS_RESIZED }, () => {
|
||||
this.resizeFrameId = raf(() => {
|
||||
this.setState({ resizeStatus: RESIZE_STATUS_NONE });
|
||||
this.fixFirefoxAutoScroll();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
raf.cancel(this.nextFrameActionId);
|
||||
raf.cancel(this.resizeFrameId);
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/21870
|
||||
fixFirefoxAutoScroll() {
|
||||
try {
|
||||
if (document.activeElement === this.textArea) {
|
||||
const currentStart = this.textArea.selectionStart;
|
||||
const currentEnd = this.textArea.selectionEnd;
|
||||
this.textArea.setSelectionRange(currentStart, currentEnd);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fix error in Chrome:
|
||||
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
|
||||
// http://stackoverflow.com/q/21177489/3040605
|
||||
}
|
||||
}
|
||||
|
||||
renderTextArea = () => {
|
||||
const { prefixCls, autoSize, onResize, className, disabled } = this.props;
|
||||
const { textareaStyles, resizeStatus } = this.state;
|
||||
const otherProps = omit(this.props, [
|
||||
'prefixCls',
|
||||
'onPressEnter',
|
||||
'autoSize',
|
||||
'defaultValue',
|
||||
'allowClear',
|
||||
'onResize',
|
||||
]);
|
||||
const cls = classNames(prefixCls, className, {
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
});
|
||||
// Fix https://github.com/ant-design/ant-design/issues/6776
|
||||
// Make sure it could be reset when using form.getFieldDecorator
|
||||
if ('value' in otherProps) {
|
||||
otherProps.value = otherProps.value || '';
|
||||
}
|
||||
const style = {
|
||||
...this.props.style,
|
||||
...textareaStyles,
|
||||
...(resizeStatus === RESIZE_STATUS_RESIZING
|
||||
? // React will warning when mix `overflow` & `overflowY`.
|
||||
// We need to define this separately.
|
||||
{ overflowX: 'hidden', overflowY: 'hidden' }
|
||||
: null),
|
||||
};
|
||||
return (
|
||||
<ResizeObserver onResize={this.handleResize} disabled={!(autoSize || onResize)}>
|
||||
<textarea {...otherProps} className={cls} style={style} ref={this.saveTextArea} />
|
||||
</ResizeObserver>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderTextArea();
|
||||
}
|
||||
}
|
||||
|
||||
export default ResizableTextArea;
|
@ -1,17 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import RcTextArea, { TextAreaProps as RcTextAreaProps, ResizableTextArea } from 'rc-textarea';
|
||||
import omit from 'omit.js';
|
||||
import ClearableLabeledInput from './ClearableLabeledInput';
|
||||
import ResizableTextArea, { AutoSizeType } from './ResizableTextArea';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { fixControlledValue, resolveOnChange } from './Input';
|
||||
|
||||
export type HTMLTextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export interface TextAreaProps extends HTMLTextareaProps {
|
||||
prefixCls?: string;
|
||||
autoSize?: boolean | AutoSizeType;
|
||||
onPressEnter?: React.KeyboardEventHandler<HTMLTextAreaElement>;
|
||||
export interface TextAreaProps extends RcTextAreaProps {
|
||||
allowClear?: boolean;
|
||||
onResize?: (size: { width: number; height: number }) => void;
|
||||
}
|
||||
|
||||
export interface TextAreaState {
|
||||
@ -54,8 +49,8 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
this.resizableTextArea.textArea.blur();
|
||||
}
|
||||
|
||||
saveTextArea = (resizableTextArea: ResizableTextArea) => {
|
||||
this.resizableTextArea = resizableTextArea;
|
||||
saveTextArea = (textarea: RcTextArea) => {
|
||||
this.resizableTextArea = textarea?.resizableTextArea;
|
||||
};
|
||||
|
||||
saveClearableInput = (clearableInput: ClearableLabeledInput) => {
|
||||
@ -63,25 +58,12 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
};
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setValue(e.target.value, () => {
|
||||
this.resizableTextArea.resizeTextarea();
|
||||
});
|
||||
this.setValue(e.target.value);
|
||||
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
|
||||
};
|
||||
|
||||
handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const { onPressEnter, onKeyDown } = this.props;
|
||||
if (e.keyCode === 13 && onPressEnter) {
|
||||
onPressEnter(e);
|
||||
}
|
||||
if (onKeyDown) {
|
||||
onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
this.setValue('', () => {
|
||||
this.resizableTextArea.renderTextArea();
|
||||
this.focus();
|
||||
});
|
||||
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
|
||||
@ -89,10 +71,9 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
|
||||
renderTextArea = (prefixCls: string) => {
|
||||
return (
|
||||
<ResizableTextArea
|
||||
{...this.props}
|
||||
<RcTextArea
|
||||
{...omit(this.props, ['allowClear'])}
|
||||
prefixCls={prefixCls}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleChange}
|
||||
ref={this.saveTextArea}
|
||||
/>
|
||||
|
@ -252,6 +252,7 @@ Array [
|
||||
style="width:100px"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
/>
|
||||
</div>,
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import RcTextArea from 'rc-textarea';
|
||||
import Input from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import calculateNodeHeight, { calculateNodeStyling } from '../calculateNodeHeight';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@ -78,56 +77,14 @@ describe('TextArea', () => {
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calculateNodeStyling works correctly', () => {
|
||||
const wrapper = document.createElement('textarea');
|
||||
wrapper.id = 'test';
|
||||
wrapper.wrap = 'wrap';
|
||||
calculateNodeStyling(wrapper, true);
|
||||
const value = calculateNodeStyling(wrapper, true);
|
||||
expect(value).toEqual({
|
||||
borderSize: 2,
|
||||
boxSizing: 'border-box',
|
||||
paddingSize: 4,
|
||||
sizingStyle:
|
||||
'letter-spacing:normal;line-height:normal;padding-top:2px;padding-bottom:2px;font-family:-webkit-small-control;font-weight:;font-size:;font-variant:;text-rendering:auto;text-transform:none;width:;text-indent:0;padding-left:2px;padding-right:2px;border-width:1px;box-sizing:border-box',
|
||||
});
|
||||
});
|
||||
|
||||
it('boxSizing === "border-box"', () => {
|
||||
const wrapper = document.createElement('textarea');
|
||||
wrapper.style.boxSizing = 'border-box';
|
||||
const { height } = calculateNodeHeight(wrapper);
|
||||
expect(height).toBe(2);
|
||||
});
|
||||
|
||||
it('boxSizing === "content-box"', () => {
|
||||
const wrapper = document.createElement('textarea');
|
||||
wrapper.style.boxSizing = 'content-box';
|
||||
const { height } = calculateNodeHeight(wrapper);
|
||||
expect(height).toBe(-4);
|
||||
});
|
||||
|
||||
it('minRows or maxRows is not null', () => {
|
||||
const wrapper = document.createElement('textarea');
|
||||
expect(calculateNodeHeight(wrapper, 1, 1)).toEqual({
|
||||
height: 2,
|
||||
maxHeight: 9007199254740991,
|
||||
minHeight: 2,
|
||||
overflowY: undefined,
|
||||
});
|
||||
wrapper.style.boxSizing = 'content-box';
|
||||
expect(calculateNodeHeight(wrapper, 1, 1)).toEqual({
|
||||
height: -4,
|
||||
maxHeight: 9007199254740991,
|
||||
minHeight: -4,
|
||||
overflowY: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('when prop value not in this.props, resizeTextarea should be called', () => {
|
||||
it('when prop value not in this.props, resizeTextarea should be called', async () => {
|
||||
const wrapper = mount(<TextArea aria-label="textarea" />);
|
||||
const resizeTextarea = jest.spyOn(wrapper.instance().resizableTextArea, 'resizeTextarea');
|
||||
wrapper.find('textarea').simulate('change', 'test');
|
||||
wrapper.find('textarea').simulate('change', {
|
||||
target: {
|
||||
value: 'test',
|
||||
},
|
||||
});
|
||||
expect(resizeTextarea).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -137,7 +94,7 @@ describe('TextArea', () => {
|
||||
const wrapper = mount(
|
||||
<TextArea onPressEnter={onPressEnter} onKeyDown={onKeyDown} aria-label="textarea" />,
|
||||
);
|
||||
wrapper.instance().handleKeyDown({ keyCode: 13 });
|
||||
wrapper.find(RcTextArea).instance().handleKeyDown({ keyCode: 13 });
|
||||
expect(onPressEnter).toHaveBeenCalled();
|
||||
expect(onKeyDown).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -1,156 +0,0 @@
|
||||
// Thanks to https://github.com/andreypopp/react-textarea-autosize/
|
||||
|
||||
/**
|
||||
* calculateNodeHeight(uiTextNode, useCache = false)
|
||||
*/
|
||||
|
||||
const HIDDEN_TEXTAREA_STYLE = `
|
||||
min-height:0 !important;
|
||||
max-height:none !important;
|
||||
height:0 !important;
|
||||
visibility:hidden !important;
|
||||
overflow:hidden !important;
|
||||
position:absolute !important;
|
||||
z-index:-1000 !important;
|
||||
top:0 !important;
|
||||
right:0 !important
|
||||
`;
|
||||
|
||||
const SIZING_STYLE = [
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'padding-top',
|
||||
'padding-bottom',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'font-size',
|
||||
'font-variant',
|
||||
'text-rendering',
|
||||
'text-transform',
|
||||
'width',
|
||||
'text-indent',
|
||||
'padding-left',
|
||||
'padding-right',
|
||||
'border-width',
|
||||
'box-sizing',
|
||||
];
|
||||
|
||||
export interface NodeType {
|
||||
sizingStyle: string;
|
||||
paddingSize: number;
|
||||
borderSize: number;
|
||||
boxSizing: string;
|
||||
}
|
||||
|
||||
const computedStyleCache: { [key: string]: NodeType } = {};
|
||||
let hiddenTextarea: HTMLTextAreaElement;
|
||||
|
||||
export function calculateNodeStyling(node: HTMLElement, useCache = false) {
|
||||
const nodeRef = (node.getAttribute('id') ||
|
||||
node.getAttribute('data-reactid') ||
|
||||
node.getAttribute('name')) as string;
|
||||
|
||||
if (useCache && computedStyleCache[nodeRef]) {
|
||||
return computedStyleCache[nodeRef];
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(node);
|
||||
|
||||
const boxSizing =
|
||||
style.getPropertyValue('box-sizing') ||
|
||||
style.getPropertyValue('-moz-box-sizing') ||
|
||||
style.getPropertyValue('-webkit-box-sizing');
|
||||
|
||||
const paddingSize =
|
||||
parseFloat(style.getPropertyValue('padding-bottom')) +
|
||||
parseFloat(style.getPropertyValue('padding-top'));
|
||||
|
||||
const borderSize =
|
||||
parseFloat(style.getPropertyValue('border-bottom-width')) +
|
||||
parseFloat(style.getPropertyValue('border-top-width'));
|
||||
|
||||
const sizingStyle = SIZING_STYLE.map(name => `${name}:${style.getPropertyValue(name)}`).join(';');
|
||||
|
||||
const nodeInfo: NodeType = {
|
||||
sizingStyle,
|
||||
paddingSize,
|
||||
borderSize,
|
||||
boxSizing,
|
||||
};
|
||||
|
||||
if (useCache && nodeRef) {
|
||||
computedStyleCache[nodeRef] = nodeInfo;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
export default function calculateNodeHeight(
|
||||
uiTextNode: HTMLTextAreaElement,
|
||||
useCache = false,
|
||||
minRows: number | null = null,
|
||||
maxRows: number | null = null,
|
||||
) {
|
||||
if (!hiddenTextarea) {
|
||||
hiddenTextarea = document.createElement('textarea');
|
||||
hiddenTextarea.setAttribute('tab-index', '-1');
|
||||
hiddenTextarea.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(hiddenTextarea);
|
||||
}
|
||||
|
||||
// Fix wrap="off" issue
|
||||
// https://github.com/ant-design/ant-design/issues/6577
|
||||
if (uiTextNode.getAttribute('wrap')) {
|
||||
hiddenTextarea.setAttribute('wrap', uiTextNode.getAttribute('wrap') as string);
|
||||
} else {
|
||||
hiddenTextarea.removeAttribute('wrap');
|
||||
}
|
||||
|
||||
// Copy all CSS properties that have an impact on the height of the content in
|
||||
// the textbox
|
||||
const { paddingSize, borderSize, boxSizing, sizingStyle } = calculateNodeStyling(
|
||||
uiTextNode,
|
||||
useCache,
|
||||
);
|
||||
|
||||
// Need to have the overflow attribute to hide the scrollbar otherwise
|
||||
// text-lines will not calculated properly as the shadow will technically be
|
||||
// narrower for content
|
||||
hiddenTextarea.setAttribute('style', `${sizingStyle};${HIDDEN_TEXTAREA_STYLE}`);
|
||||
hiddenTextarea.value = uiTextNode.value || uiTextNode.placeholder || '';
|
||||
|
||||
let minHeight = Number.MIN_SAFE_INTEGER;
|
||||
let maxHeight = Number.MAX_SAFE_INTEGER;
|
||||
let height = hiddenTextarea.scrollHeight;
|
||||
let overflowY: any;
|
||||
|
||||
if (boxSizing === 'border-box') {
|
||||
// border-box: add border, since height = content + padding + border
|
||||
height += borderSize;
|
||||
} else if (boxSizing === 'content-box') {
|
||||
// remove padding, since height = content
|
||||
height -= paddingSize;
|
||||
}
|
||||
|
||||
if (minRows !== null || maxRows !== null) {
|
||||
// measure height of a textarea with a single row
|
||||
hiddenTextarea.value = ' ';
|
||||
const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
|
||||
if (minRows !== null) {
|
||||
minHeight = singleRowHeight * minRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
minHeight = minHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.max(minHeight, height);
|
||||
}
|
||||
if (maxRows !== null) {
|
||||
maxHeight = singleRowHeight * maxRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
maxHeight = maxHeight + paddingSize + borderSize;
|
||||
}
|
||||
overflowY = height > maxHeight ? '' : 'hidden';
|
||||
height = Math.min(maxHeight, height);
|
||||
}
|
||||
}
|
||||
return { height, minHeight, maxHeight, overflowY };
|
||||
}
|
@ -136,6 +136,7 @@ describe('List.pagination', () => {
|
||||
expect(wrapper.find('Pagination').first().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/24913
|
||||
// https://github.com/ant-design/ant-design/issues/24501
|
||||
it('should onChange called when pageSize change', () => {
|
||||
const handlePaginationChange = jest.fn();
|
||||
|
@ -105,11 +105,6 @@ function List<T>({
|
||||
return (page: number, pageSize: number) => {
|
||||
setPaginationCurrent(page);
|
||||
setPaginationSize(pageSize);
|
||||
if (eventName === 'onShowSizeChange') {
|
||||
if (pagination) {
|
||||
pagination?.onChange?.(page, pageSize);
|
||||
}
|
||||
}
|
||||
if (pagination && (pagination as any)[eventName]) {
|
||||
(pagination as any)[eventName](page, pageSize);
|
||||
}
|
||||
|
@ -6,6 +6,19 @@ exports[`renders ./components/mentions/demo/async.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/autoSize.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
@ -17,6 +30,7 @@ exports[`renders ./components/mentions/demo/basic.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
>
|
||||
@afc163
|
||||
@ -55,6 +69,7 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
|
||||
class="ant-mentions"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
id="coders"
|
||||
rows="1"
|
||||
/>
|
||||
@ -90,6 +105,7 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
|
||||
class="ant-mentions"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
id="bio"
|
||||
placeholder="You can use @ to ref user here"
|
||||
rows="3"
|
||||
@ -141,6 +157,7 @@ exports[`renders ./components/mentions/demo/placement.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
@ -152,6 +169,7 @@ exports[`renders ./components/mentions/demo/prefix.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
placeholder="input @ to mention people, # to mention tag"
|
||||
rows="1"
|
||||
/>
|
||||
@ -168,6 +186,7 @@ exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea rc-textarea-disabled"
|
||||
disabled=""
|
||||
placeholder="this is disabled Mentions"
|
||||
rows="1"
|
||||
@ -179,6 +198,7 @@ exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
placeholder="this is readOnly Mentions"
|
||||
readonly=""
|
||||
rows="1"
|
||||
|
@ -5,6 +5,7 @@ exports[`Mentions rtl render component should be rendered correctly in RTL direc
|
||||
class="ant-mentions ant-mentions-rtl"
|
||||
>
|
||||
<textarea
|
||||
class="rc-textarea"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
|
29
components/mentions/demo/autoSize.md
Normal file
29
components/mentions/demo/autoSize.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 自动大小
|
||||
en-US: autoSize
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
自适应内容高度。
|
||||
|
||||
## en-US
|
||||
|
||||
Height autoSize.
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
ReactDOM.render(
|
||||
<Mentions autoSize style={{ width: '100%' }}>
|
||||
<Option value="afc163">afc163</Option>
|
||||
<Option value="zombieJ">zombieJ</Option>
|
||||
<Option value="yesmeck">yesmeck</Option>
|
||||
</Mentions>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -7,7 +7,7 @@ title:
|
||||
|
||||
## zh-CN
|
||||
|
||||
基本使用
|
||||
基本使用。
|
||||
|
||||
## en-US
|
||||
|
||||
|
@ -38,6 +38,8 @@ When need to mention someone or something.
|
||||
| onFocus | Trigger when mentions get focus | () => void | |
|
||||
| onBlur | Trigger when mentions lose focus | () => void | |
|
||||
| getPopupContainer | Set the mount HTML node for suggestions | () => HTMLElement | |
|
||||
| autoSize | Textarea height autosize feature, can be set to `true\|false` or an object `{ minRows: 2, maxRows: 6 }` | boolean \| object | false |
|
||||
| onResize | The callback function that is triggered when textarea resize | function({ width, height }) | |
|
||||
|
||||
### Mention methods
|
||||
|
||||
|
@ -39,6 +39,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/jPE-itMFM/Mentions.svg
|
||||
| onFocus | 获得焦点时触发 | () => void | |
|
||||
| onBlur | 失去焦点时触发 | () => void | |
|
||||
| getPopupContainer | 指定建议框挂载的 HTML 节点 | () => HTMLElement | |
|
||||
| autoSize | 自适应内容高度,可设置为 `true|false` 或对象:`{ minRows: 2, maxRows: 6 }`。 | boolean\|object | false |
|
||||
| onResize | resize 回调 | function({ width, height }) | |
|
||||
|
||||
### Mentions 方法
|
||||
|
||||
|
@ -20,7 +20,7 @@ When requiring users to interact with the application, but without jumping to a
|
||||
| cancelText | Text of the Cancel button | string\|ReactNode | `Cancel` |
|
||||
| centered | Centered Modal | boolean | false |
|
||||
| closable | Whether a close (x) button is visible on top right of the modal dialog or not | boolean | true |
|
||||
| closeIcon | custom close icon | ReactNode | - |
|
||||
| closeIcon | custom close icon | ReactNode | `<CloseOutlined />` |
|
||||
| confirmLoading | Whether to apply loading visual effect for OK button or not | boolean | false |
|
||||
| destroyOnClose | Whether to unmount child components on onClose | boolean | false |
|
||||
| footer | Footer content, set as `footer={null}` when you don't need default buttons | string\|ReactNode | OK and Cancel buttons |
|
||||
|
@ -23,7 +23,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3StSdUlSH/Modal.svg
|
||||
| cancelText | 取消按钮文字 | string\|ReactNode | 取消 |
|
||||
| centered | 垂直居中展示 Modal | boolean | false |
|
||||
| closable | 是否显示右上角的关闭按钮 | boolean | true |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | - |
|
||||
| closeIcon | 自定义关闭图标 | ReactNode | `<CloseOutlined />` |
|
||||
| confirmLoading | 确定按钮 loading | boolean | false |
|
||||
| destroyOnClose | 关闭时销毁 Modal 里的子元素 | boolean | false |
|
||||
| footer | 底部内容,当不需要默认底部按钮时,可以设为 `footer={null}` | string\|ReactNode | 确定取消按钮 |
|
||||
|
@ -27,8 +27,8 @@
|
||||
margin: 0;
|
||||
color: @modal-heading-color;
|
||||
font-weight: 500;
|
||||
font-size: @font-size-lg;
|
||||
line-height: 22px;
|
||||
font-size: @modal-header-title-font-size;
|
||||
line-height: @modal-header-title-line-height;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
padding: @modal-header-padding;
|
||||
color: @text-color;
|
||||
background: @modal-header-bg;
|
||||
border-bottom: @border-width-base @border-style-base @modal-header-border-color-split;
|
||||
border-bottom: @modal-header-border-width @modal-header-border-style @modal-header-border-color-split;
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
padding: @modal-footer-padding-vertical @modal-footer-padding-horizontal;
|
||||
text-align: right;
|
||||
background: @modal-footer-bg;
|
||||
border-top: @border-width-base @border-style-base @modal-footer-border-color-split;
|
||||
border-top: @modal-footer-border-width @modal-footer-border-style @modal-footer-border-color-split;
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
|
||||
button + button {
|
||||
|
@ -29,21 +29,30 @@ describe('Pagination', () => {
|
||||
return originalElement;
|
||||
}
|
||||
const wrapper = mount(<Pagination defaultCurrent={1} total={50} itemRender={itemRender} />);
|
||||
expect(
|
||||
wrapper
|
||||
.find('button')
|
||||
.at(0)
|
||||
.props().disabled,
|
||||
).toBe(true);
|
||||
expect(wrapper.find('button').at(0).props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should autometically be small when size is not specified', async () => {
|
||||
const wrapper = mount(<Pagination responsive />);
|
||||
expect(
|
||||
wrapper
|
||||
.find('ul')
|
||||
.at(0)
|
||||
.hasClass('mini'),
|
||||
).toBe(true);
|
||||
expect(wrapper.find('ul').at(0).hasClass('mini')).toBe(true);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/24913
|
||||
// https://github.com/ant-design/ant-design/issues/24501
|
||||
it('should onChange called when pageSize change', () => {
|
||||
const onChange = jest.fn();
|
||||
const onShowSizeChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
total={500}
|
||||
onChange={onChange}
|
||||
onShowSizeChange={onShowSizeChange}
|
||||
/>,
|
||||
);
|
||||
wrapper.find('.ant-select-selector').simulate('mousedown');
|
||||
expect(wrapper.find('.ant-select-item-option').length).toBe(4);
|
||||
wrapper.find('.ant-select-item-option').at(1).simulate('click');
|
||||
expect(onChange).toHaveBeenCalledWith(1, 20);
|
||||
});
|
||||
});
|
||||
|
@ -10,8 +10,11 @@ interface CircleProps extends ProgressProps {
|
||||
progressStatus: string;
|
||||
}
|
||||
|
||||
function getPercentage({ percent, successPercent }: CircleProps) {
|
||||
function getPercentage({ percent, success, successPercent }: CircleProps) {
|
||||
const ptg = validProgress(percent);
|
||||
if (success && 'progress' in success) {
|
||||
successPercent = success.progress;
|
||||
}
|
||||
if (!successPercent) {
|
||||
return ptg;
|
||||
}
|
||||
@ -20,8 +23,11 @@ function getPercentage({ percent, successPercent }: CircleProps) {
|
||||
return [successPercent, validProgress(ptg - successPtg)];
|
||||
}
|
||||
|
||||
function getStrokeColor({ successPercent, strokeColor }: CircleProps) {
|
||||
function getStrokeColor({ success, strokeColor, successPercent }: CircleProps) {
|
||||
const color = strokeColor || null;
|
||||
if (success && 'progress' in success) {
|
||||
successPercent = success.progress;
|
||||
}
|
||||
if (!successPercent) {
|
||||
return color;
|
||||
}
|
||||
|
@ -61,14 +61,15 @@ const Line: React.FC<LineProps> = props => {
|
||||
const {
|
||||
prefixCls,
|
||||
percent,
|
||||
successPercent,
|
||||
strokeWidth,
|
||||
size,
|
||||
strokeColor,
|
||||
strokeLinecap,
|
||||
children,
|
||||
trailColor,
|
||||
success,
|
||||
} = props;
|
||||
|
||||
let backgroundProps;
|
||||
if (strokeColor && typeof strokeColor !== 'string') {
|
||||
backgroundProps = handleGradient(strokeColor);
|
||||
@ -77,23 +78,45 @@ const Line: React.FC<LineProps> = props => {
|
||||
background: strokeColor,
|
||||
};
|
||||
}
|
||||
|
||||
let trailStyle;
|
||||
if (trailColor && typeof trailColor === 'string') {
|
||||
trailStyle = {
|
||||
backgroundColor: trailColor,
|
||||
};
|
||||
}
|
||||
|
||||
let successColor;
|
||||
if (success && 'strokeColor' in success) {
|
||||
successColor = success.strokeColor;
|
||||
}
|
||||
|
||||
let successStyle;
|
||||
if (successColor && typeof successColor === 'string') {
|
||||
successStyle = {
|
||||
backgroundColor: successColor,
|
||||
};
|
||||
}
|
||||
const percentStyle = {
|
||||
width: `${validProgress(percent)}%`,
|
||||
height: strokeWidth || (size === 'small' ? 6 : 8),
|
||||
borderRadius: strokeLinecap === 'square' ? 0 : '',
|
||||
...backgroundProps,
|
||||
};
|
||||
const successPercentStyle = {
|
||||
|
||||
let { successPercent } = props;
|
||||
if (success && 'progress' in success) {
|
||||
successPercent = success.progress;
|
||||
}
|
||||
|
||||
let successPercentStyle = {
|
||||
width: `${validProgress(successPercent)}%`,
|
||||
height: strokeWidth || (size === 'small' ? 6 : 8),
|
||||
borderRadius: strokeLinecap === 'square' ? 0 : '',
|
||||
};
|
||||
if (successStyle) {
|
||||
successPercentStyle = { ...successPercentStyle, ...successStyle };
|
||||
}
|
||||
const successSegment =
|
||||
successPercent !== undefined ? (
|
||||
<div className={`${prefixCls}-success-bg`} style={successPercentStyle} />
|
||||
|
@ -500,6 +500,35 @@ exports[`Progress render strokeColor 3`] = `
|
||||
</Progress>
|
||||
`;
|
||||
|
||||
exports[`Progress render successColor progress 1`] = `
|
||||
<div
|
||||
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-outer"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-inner"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-bg"
|
||||
style="width: 60%; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-success-bg"
|
||||
style="width: 30%; height: 8px; background-color: rgb(255, 255, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
title="60%"
|
||||
>
|
||||
60%
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Progress render trailColor progress 1`] = `
|
||||
<div
|
||||
class="ant-progress ant-progress-line ant-progress-status-normal ant-progress-show-info ant-progress-default"
|
||||
|
@ -10,13 +10,13 @@ describe('Progress', () => {
|
||||
rtlTest(Progress);
|
||||
|
||||
it('successPercent should decide the progress status when it exists', () => {
|
||||
const wrapper = mount(<Progress percent={100} successPercent={50} />);
|
||||
const wrapper = mount(<Progress percent={100} success={{ progress: 50 }} />);
|
||||
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0);
|
||||
|
||||
wrapper.setProps({ percent: 50, successPercent: 100 });
|
||||
wrapper.setProps({ percent: 50, success: { progress: 100 } });
|
||||
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(1);
|
||||
|
||||
wrapper.setProps({ percent: 100, successPercent: 0 });
|
||||
wrapper.setProps({ percent: 100, success: { progress: 0 } });
|
||||
expect(wrapper.find('.ant-progress-status-success')).toHaveLength(0);
|
||||
});
|
||||
|
||||
@ -36,7 +36,7 @@ describe('Progress', () => {
|
||||
});
|
||||
|
||||
it('render negative successPercent', () => {
|
||||
const wrapper = mount(<Progress percent={50} successPercent={-20} />);
|
||||
const wrapper = mount(<Progress percent={50} success={{ progress: -20 }} />);
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ describe('Progress', () => {
|
||||
const wrapper = mount(
|
||||
<Progress
|
||||
percent={50}
|
||||
successPercent={10}
|
||||
success={{ progress: 10 }}
|
||||
format={(percent, successPercent) => `${percent} ${successPercent}`}
|
||||
/>,
|
||||
);
|
||||
@ -81,6 +81,13 @@ describe('Progress', () => {
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render successColor progress', () => {
|
||||
const wrapper = mount(
|
||||
<Progress percent={60} success={{ progress: 30, strokeColor: '#ffffff' }} />,
|
||||
);
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render dashboard zero gapDegree', () => {
|
||||
const wrapper = mount(<Progress type="dashboard" gapDegree={0} />);
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
|
@ -19,15 +19,15 @@ import { Tooltip, Progress } from 'antd';
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<Tooltip title="3 done / 3 in progress / 4 to do">
|
||||
<Progress percent={60} successPercent={30} />
|
||||
<Progress percent={60} success={{ progress: 30 }} />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="3 done / 3 in progress / 4 to do">
|
||||
<Progress percent={60} successPercent={30} type="circle" />
|
||||
<Progress percent={60} success={{ progress: 30 }} type="circle" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="3 done / 3 in progress / 4 to do">
|
||||
<Progress percent={60} successPercent={30} type="dashboard" />
|
||||
<Progress percent={60} success={{ progress: 30 }} type="dashboard" />
|
||||
</Tooltip>
|
||||
</>,
|
||||
mountNode,
|
||||
|
@ -27,8 +27,8 @@ Properties that shared by all types.
|
||||
| status | to set the status of the Progress, options: `success` `exception` `normal` `active`(line only) | string | - |
|
||||
| strokeLinecap | to set the style of the progress linecap | `round` \| `square` | `round` |
|
||||
| strokeColor | color of progress bar | string | - |
|
||||
| successPercent | segmented success percent | number | 0 |
|
||||
| trailColor | color of unfilled part | string | - |
|
||||
| success | configs of successfully progress bar | { progress: number, strokeColor: string } | - |
|
||||
|
||||
### `type="line"`
|
||||
|
||||
|
@ -28,8 +28,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xqsDu4ZyR/Progress.svg
|
||||
| status | 状态,可选:`success` `exception` `normal` `active`(仅限 line) | string | - |
|
||||
| strokeLinecap | - | `round` \| `square` | `round` |
|
||||
| strokeColor | 进度条的色彩 | string | - |
|
||||
| successPercent | 已完成的分段百分比 | number | 0 |
|
||||
| trailColor | 未完成的分段的颜色 | string | - |
|
||||
| success | 成功进度条相关配置 | { progress: number, strokeColor: string } | - |
|
||||
|
||||
### `type="line"`
|
||||
|
||||
|
@ -8,6 +8,7 @@ import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { tuple } from '../_util/type';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import Line from './Line';
|
||||
import Circle from './Circle';
|
||||
import Steps from './Steps';
|
||||
@ -20,12 +21,17 @@ export type ProgressSize = 'default' | 'small';
|
||||
export type StringGradients = { [percentage: string]: string };
|
||||
type FromToGradients = { from: string; to: string };
|
||||
export type ProgressGradient = { direction?: string } & (StringGradients | FromToGradients);
|
||||
|
||||
export interface SuccessProps {
|
||||
progress?: number;
|
||||
strokeColor?: string;
|
||||
}
|
||||
|
||||
export interface ProgressProps {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
type?: ProgressType;
|
||||
percent?: number;
|
||||
successPercent?: number;
|
||||
format?: (percent?: number, successPercent?: number) => React.ReactNode;
|
||||
status?: typeof ProgressStatuses[number];
|
||||
showInfo?: boolean;
|
||||
@ -34,11 +40,14 @@ export interface ProgressProps {
|
||||
strokeColor?: string | ProgressGradient;
|
||||
trailColor?: string;
|
||||
width?: number;
|
||||
success?: SuccessProps;
|
||||
style?: React.CSSProperties;
|
||||
gapDegree?: number;
|
||||
gapPosition?: 'top' | 'bottom' | 'left' | 'right';
|
||||
size?: ProgressSize;
|
||||
steps?: number;
|
||||
/** @deprecated Use `success` instead */
|
||||
successPercent?: number;
|
||||
}
|
||||
|
||||
export default class Progress extends React.Component<ProgressProps> {
|
||||
@ -54,7 +63,11 @@ export default class Progress extends React.Component<ProgressProps> {
|
||||
};
|
||||
|
||||
getPercentNumber() {
|
||||
const { successPercent, percent = 0 } = this.props;
|
||||
const { percent = 0, success } = this.props;
|
||||
let { successPercent } = this.props;
|
||||
if (success && 'progress' in success) {
|
||||
successPercent = success.progress;
|
||||
}
|
||||
return parseInt(
|
||||
successPercent !== undefined ? successPercent.toString() : percent.toString(),
|
||||
10,
|
||||
@ -70,7 +83,11 @@ export default class Progress extends React.Component<ProgressProps> {
|
||||
}
|
||||
|
||||
renderProcessInfo(prefixCls: string, progressStatus: typeof ProgressStatuses[number]) {
|
||||
const { showInfo, format, type, percent, successPercent } = this.props;
|
||||
const { showInfo, format, type, percent, success } = this.props;
|
||||
let { successPercent } = this.props;
|
||||
if (success && 'progress' in success) {
|
||||
successPercent = success.progress;
|
||||
}
|
||||
if (!showInfo) return null;
|
||||
|
||||
let text;
|
||||
@ -105,6 +122,13 @@ export default class Progress extends React.Component<ProgressProps> {
|
||||
const prefixCls = getPrefixCls('progress', customizePrefixCls);
|
||||
const progressStatus = this.getProgressStatus();
|
||||
const progressInfo = this.renderProcessInfo(prefixCls, progressStatus);
|
||||
|
||||
devWarning(
|
||||
'successPercent' in props,
|
||||
'Progress',
|
||||
'`successPercent` is deprecated. Please use `success` instead.',
|
||||
);
|
||||
|
||||
let progress;
|
||||
// Render progress shape
|
||||
if (type === 'line') {
|
||||
@ -148,7 +172,6 @@ export default class Progress extends React.Component<ProgressProps> {
|
||||
'status',
|
||||
'format',
|
||||
'trailColor',
|
||||
'successPercent',
|
||||
'strokeWidth',
|
||||
'width',
|
||||
'gapDegree',
|
||||
@ -157,6 +180,8 @@ export default class Progress extends React.Component<ProgressProps> {
|
||||
'strokeLinecap',
|
||||
'percent',
|
||||
'steps',
|
||||
'success',
|
||||
'successPercent',
|
||||
])}
|
||||
className={classString}
|
||||
>
|
||||
|
@ -1110,6 +1110,7 @@ Array [
|
||||
</span>
|
||||
</label>
|
||||
</div>,
|
||||
<br />,
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
@ -1153,13 +1154,14 @@ Array [
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-wrapper"
|
||||
class="ant-radio-wrapper ant-radio-wrapper-disabled"
|
||||
>
|
||||
<span
|
||||
class="ant-radio"
|
||||
class="ant-radio ant-radio-disabled"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-input"
|
||||
disabled=""
|
||||
type="radio"
|
||||
value="Orange"
|
||||
/>
|
||||
@ -1172,23 +1174,25 @@ Array [
|
||||
</span>
|
||||
</label>
|
||||
</div>,
|
||||
<br />,
|
||||
<br />,
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
<label
|
||||
class="ant-radio-wrapper ant-radio-wrapper-checked"
|
||||
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
|
||||
>
|
||||
<span
|
||||
class="ant-radio ant-radio-checked"
|
||||
class="ant-radio-button ant-radio-button-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-radio-input"
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="Apple"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-inner"
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
@ -1196,18 +1200,18 @@ Array [
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-wrapper"
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio"
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-input"
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="Pear"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-inner"
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
@ -1215,18 +1219,83 @@ Array [
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-wrapper"
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio"
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-input"
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="Orange"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-inner"
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Orange
|
||||
</span>
|
||||
</label>
|
||||
</div>,
|
||||
<br />,
|
||||
<br />,
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-solid"
|
||||
>
|
||||
<label
|
||||
class="ant-radio-button-wrapper ant-radio-button-wrapper-checked"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button ant-radio-button-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="Apple"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Apple
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
type="radio"
|
||||
value="Pear"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Pear
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="ant-radio-button-wrapper ant-radio-button-wrapper-disabled"
|
||||
>
|
||||
<span
|
||||
class="ant-radio-button ant-radio-button-disabled"
|
||||
>
|
||||
<input
|
||||
class="ant-radio-button-input"
|
||||
disabled=""
|
||||
type="radio"
|
||||
value="Orange"
|
||||
/>
|
||||
<span
|
||||
class="ant-radio-button-inner"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
|
@ -7,11 +7,11 @@ title:
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过配置 `options` 参数来渲染单选框。
|
||||
通过配置 `options` 参数来渲染单选框。也可通过 `optionType` 参数来设置 Radio 类型。
|
||||
|
||||
## en-US
|
||||
|
||||
Render radios by configuring `options`.
|
||||
Render radios by configuring `options`. Radio type can also be set through the `optionType` parameter.
|
||||
|
||||
```jsx
|
||||
import { Radio } from 'antd';
|
||||
@ -25,7 +25,7 @@ const options = [
|
||||
const optionsWithDisabled = [
|
||||
{ label: 'Apple', value: 'Apple' },
|
||||
{ label: 'Pear', value: 'Pear' },
|
||||
{ label: 'Orange', value: 'Orange', disabled: false },
|
||||
{ label: 'Orange', value: 'Orange', disabled: true },
|
||||
];
|
||||
|
||||
class App extends React.Component {
|
||||
@ -33,6 +33,7 @@ class App extends React.Component {
|
||||
value1: 'Apple',
|
||||
value2: 'Apple',
|
||||
value3: 'Apple',
|
||||
value4: 'Apple',
|
||||
};
|
||||
|
||||
onChange1 = e => {
|
||||
@ -56,16 +57,36 @@ class App extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
onChange4 = e => {
|
||||
console.log('radio4 checked', e.target.value);
|
||||
this.setState({
|
||||
value4: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value1, value2, value3 } = this.state;
|
||||
const { value1, value2, value3, value4 } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Radio.Group options={plainOptions} onChange={this.onChange1} value={value1} />
|
||||
<Radio.Group options={options} onChange={this.onChange2} value={value2} />
|
||||
<br />
|
||||
<Radio.Group options={optionsWithDisabled} onChange={this.onChange2} value={value2} />
|
||||
<br />
|
||||
<br />
|
||||
<Radio.Group
|
||||
options={optionsWithDisabled}
|
||||
options={options}
|
||||
onChange={this.onChange3}
|
||||
value={value3}
|
||||
optionType="button"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Radio.Group
|
||||
options={optionsWithDisabled}
|
||||
onChange={this.onChange4}
|
||||
value={value4}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -43,6 +43,7 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
|
||||
prefixCls: customizePrefixCls,
|
||||
className = '',
|
||||
options,
|
||||
optionType,
|
||||
buttonStyle,
|
||||
disabled,
|
||||
children,
|
||||
@ -57,13 +58,14 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
|
||||
let childrenToRender = children;
|
||||
// 如果存在 options, 优先使用
|
||||
if (options && options.length > 0) {
|
||||
const optionsPrefixCls = optionType === 'button' ? `${prefixCls}-button` : prefixCls;
|
||||
childrenToRender = options.map(option => {
|
||||
if (typeof option === 'string') {
|
||||
// 此处类型自动推导为 string
|
||||
return (
|
||||
<Radio
|
||||
key={option}
|
||||
prefixCls={prefixCls}
|
||||
prefixCls={optionsPrefixCls}
|
||||
disabled={disabled}
|
||||
value={option}
|
||||
checked={value === option}
|
||||
@ -76,7 +78,7 @@ const RadioGroup: React.FC<RadioGroupProps> = props => {
|
||||
return (
|
||||
<Radio
|
||||
key={`radio-group-value-options-${option.value}`}
|
||||
prefixCls={prefixCls}
|
||||
prefixCls={optionsPrefixCls}
|
||||
disabled={option.disabled || disabled}
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
|
@ -28,16 +28,17 @@ Radio.
|
||||
|
||||
Radio group can wrap a group of `Radio`。
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| defaultValue | Default selected value | any | |
|
||||
| disabled | Disable all radio buttons | boolean | false |
|
||||
| name | The `name` property of all `input[type="radio"]` children | string | |
|
||||
| options | set children optional | string\[] \| Array<{ label: string value: string disabled?: boolean }> | |
|
||||
| size | size for radio button style | `large` \| `middle` \| `small` | |
|
||||
| value | Used for setting the currently selected value. | any | |
|
||||
| onChange | The callback function that is triggered when the state changes. | Function(e:Event) | |
|
||||
| buttonStyle | style type of radio button | `outline` \| `solid` | `outline` |
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | Default selected value | any | | |
|
||||
| disabled | Disable all radio buttons | boolean | false | |
|
||||
| name | The `name` property of all `input[type="radio"]` children | string | | |
|
||||
| options | set children optional | string\[] \| Array<{ label: string value: string disabled?: boolean }> | | |
|
||||
| size | size for radio button style | `large` \| `middle` \| `small` | | |
|
||||
| value | Used for setting the currently selected value. | any | | |
|
||||
| onChange | The callback function that is triggered when the state changes. | Function(e:Event) | | |
|
||||
| optionType | set Radio optionType | `default` \| `button` | `default` | 4.4.0 |
|
||||
| buttonStyle | style type of radio button | `outline` \| `solid` | `outline` | |
|
||||
|
||||
## Methods
|
||||
|
||||
|
@ -29,16 +29,17 @@ cover: https://gw.alipayobjects.com/zos/alicdn/8cYb5seNB/Radio.svg
|
||||
|
||||
单选框组合,用于包裹一组 `Radio`。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| defaultValue | 默认选中的值 | any | |
|
||||
| disabled | 禁选所有子单选器 | boolean | false |
|
||||
| name | RadioGroup 下所有 `input[type="radio"]` 的 `name` 属性 | string | |
|
||||
| options | 以配置形式设置子元素 | string\[] \| Array<{ label: string value: string disabled?: boolean }> | |
|
||||
| size | 大小,只对按钮样式生效 | `large` \| `middle` \| `small` | - |
|
||||
| value | 用于设置当前选中的值 | any | |
|
||||
| onChange | 选项变化时的回调函数 | Function(e:Event) | |
|
||||
| buttonStyle | RadioButton 的风格样式,目前有描边和填色两种风格 | `outline` \| `solid` | `outline` |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | 默认选中的值 | any | | |
|
||||
| disabled | 禁选所有子单选器 | boolean | false | | |
|
||||
| name | RadioGroup 下所有 `input[type="radio"]` 的 `name` 属性 | string | | |
|
||||
| options | 以配置形式设置子元素 | string\[] \| Array<{ label: string value: string disabled?: boolean }> | | |
|
||||
| size | 大小,只对按钮样式生效 | `large` \| `middle` \| `small` | - | |
|
||||
| value | 用于设置当前选中的值 | any | | |
|
||||
| onChange | 选项变化时的回调函数 | Function(e:Event) | | |
|
||||
| optionType | 用于设置 Radio `options` 类型 | `default` \| `button` | `default` | 4.4.0 |
|
||||
| buttonStyle | RadioButton 的风格样式,目前有描边和填色两种风格 | `outline` \| `solid` | `outline` | |
|
||||
|
||||
## 方法
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { AbstractCheckboxProps } from '../checkbox/Checkbox';
|
||||
import { SizeType } from '../config-provider/SizeContext';
|
||||
|
||||
export type RadioGroupButtonStyle = 'outline' | 'solid';
|
||||
export type RadioGroupOptionType = 'default' | 'button';
|
||||
|
||||
export interface RadioGroupProps extends AbstractCheckboxGroupProps {
|
||||
defaultValue?: any;
|
||||
@ -15,6 +16,7 @@ export interface RadioGroupProps extends AbstractCheckboxGroupProps {
|
||||
name?: string;
|
||||
children?: React.ReactNode;
|
||||
id?: string;
|
||||
optionType?: RadioGroupOptionType;
|
||||
buttonStyle?: RadioGroupButtonStyle;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { RadioProps, RadioChangeEvent } from './interface';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import RadioGroupContext from './context';
|
||||
import { composeRef } from '../_util/ref';
|
||||
import devWarning from '../_util/devWarning';
|
||||
|
||||
const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (props, ref) => {
|
||||
const context = React.useContext(RadioGroupContext);
|
||||
@ -12,6 +13,10 @@ const InternalRadio: React.ForwardRefRenderFunction<unknown, RadioProps> = (prop
|
||||
const innerRef = React.useRef<HTMLElement>();
|
||||
const mergedRef = composeRef(ref, innerRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
devWarning(!('optionType' in props), 'Radio', '`optionType` is only support in Radio.Group.');
|
||||
}, []);
|
||||
|
||||
const onChange = (e: RadioChangeEvent) => {
|
||||
if (props.onChange) {
|
||||
props.onChange(e);
|
||||
|
@ -310,7 +310,7 @@ exports[`renders ./components/rate/demo/basic.md correctly 1`] = `
|
||||
`;
|
||||
|
||||
exports[`renders ./components/rate/demo/character.md correctly 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
@ -616,8 +616,8 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
</ul>,
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
@ -734,8 +734,8 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
</ul>,
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
@ -851,12 +851,440 @@ exports[`renders ./components/rate/demo/character.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ul>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/rate/demo/character-function.md correctly 1`] = `
|
||||
Array [
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-full"
|
||||
>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-posinset="1"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-full"
|
||||
>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-posinset="2"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-zero"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-posinset="3"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-zero"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-posinset="4"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
4
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-zero"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-posinset="5"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
5
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
5
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>,
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-full"
|
||||
>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-posinset="1"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
<span
|
||||
aria-label="frown"
|
||||
class="anticon anticon-frown"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="frown"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
<span
|
||||
aria-label="frown"
|
||||
class="anticon anticon-frown"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="frown"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-full"
|
||||
>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-posinset="2"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
<span
|
||||
aria-label="frown"
|
||||
class="anticon anticon-frown"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="frown"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
<span
|
||||
aria-label="frown"
|
||||
class="anticon anticon-frown"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="frown"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM512 533c-85.5 0-155.6 67.3-160 151.6a8 8 0 008 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c.3 4.2 3.9 7.4 8.1 7.4H664a8 8 0 008-8.4C667.6 600.3 597.5 533 512 533z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-full"
|
||||
>
|
||||
<div
|
||||
aria-checked="true"
|
||||
aria-posinset="3"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
<span
|
||||
aria-label="meh"
|
||||
class="anticon anticon-meh"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="meh"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 565H360c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
<span
|
||||
aria-label="meh"
|
||||
class="anticon anticon-meh"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="meh"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 565H360c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-zero"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-posinset="4"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
<span
|
||||
aria-label="smile"
|
||||
class="anticon anticon-smile"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="smile"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
<span
|
||||
aria-label="smile"
|
||||
class="anticon anticon-smile"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="smile"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="ant-rate-star ant-rate-star-zero"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-posinset="5"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
>
|
||||
<span
|
||||
aria-label="smile"
|
||||
class="anticon anticon-smile"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="smile"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-rate-star-second"
|
||||
>
|
||||
<span
|
||||
aria-label="smile"
|
||||
class="anticon anticon-smile"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="smile"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
@ -1162,13 +1590,13 @@ exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>,
|
||||
<span
|
||||
class="ant-rate-text"
|
||||
>
|
||||
allowClear: true
|
||||
</span>
|
||||
<br />
|
||||
</span>,
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-rate"
|
||||
role="radiogroup"
|
||||
@ -1474,13 +1902,13 @@ exports[`renders ./components/rate/demo/clear.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>,
|
||||
<span
|
||||
class="ant-rate-text"
|
||||
>
|
||||
allowClear: false
|
||||
</span>
|
||||
</div>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
@ -1497,7 +1925,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
aria-posinset="1"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
@ -1557,7 +1985,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
aria-posinset="2"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
@ -1617,7 +2045,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
aria-posinset="3"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
@ -1677,7 +2105,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
aria-posinset="4"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
@ -1737,7 +2165,7 @@ exports[`renders ./components/rate/demo/disabled.md correctly 1`] = `
|
||||
aria-posinset="5"
|
||||
aria-setsize="5"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ant-rate-star-first"
|
||||
|
46
components/rate/demo/character-function.md
Normal file
46
components/rate/demo/character-function.md
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 自定义字符
|
||||
en-US: Customize character
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以使用 `(RateProps) => ReactNode` 的方式自定义每一个字符。
|
||||
|
||||
## en-US
|
||||
|
||||
Can customize each character using `(RateProps) => ReactNode`.
|
||||
|
||||
```jsx
|
||||
import { Rate } from 'antd';
|
||||
import { FrownOutlined, MehOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
|
||||
const customIcons = {
|
||||
1: <FrownOutlined />,
|
||||
2: <FrownOutlined />,
|
||||
3: <MehOutlined />,
|
||||
4: <SmileOutlined />,
|
||||
5: <SmileOutlined />,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<Rate
|
||||
defaultValue={2}
|
||||
character={({ index }) => {
|
||||
return index + 1;
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Rate
|
||||
defaultValue={3}
|
||||
character={({ index }) => {
|
||||
return customIcons[index + 1];
|
||||
}}
|
||||
/>
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -18,13 +18,13 @@ import { Rate } from 'antd';
|
||||
import { HeartOutlined } from '@ant-design/icons';
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<>
|
||||
<Rate character={<HeartOutlined />} allowHalf />
|
||||
<br />
|
||||
<Rate character="A" allowHalf style={{ fontSize: 36 }} />
|
||||
<br />
|
||||
<Rate character="好" allowHalf />
|
||||
</div>,
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
||||
|
@ -17,13 +17,13 @@ Support set allow to clear star when click again.
|
||||
import { Rate } from 'antd';
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<>
|
||||
<Rate defaultValue={3} />
|
||||
<span className="ant-rate-text">allowClear: true</span>
|
||||
<br />
|
||||
<Rate allowClear={false} defaultValue={3} />
|
||||
<span className="ant-rate-text">allowClear: false</span>
|
||||
</div>,
|
||||
</>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
||||
|
@ -14,24 +14,24 @@ Rate component.
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| allowClear | whether to allow clear when click again | boolean | true |
|
||||
| allowHalf | whether to allow semi selection | boolean | false |
|
||||
| autoFocus | get focus when component mounted | boolean | false |
|
||||
| character | custom character of rate | ReactNode | [`<StarFilled />`](/components/icon/) |
|
||||
| className | custom class name of rate | string | |
|
||||
| count | star count | number | 5 |
|
||||
| defaultValue | default value | number | 0 |
|
||||
| disabled | read only, unable to interact | boolean | false |
|
||||
| style | custom style object of rate | CSSProperties | |
|
||||
| tooltips | Customize tooltip by each character | string\[] | |
|
||||
| value | current value | number | |
|
||||
| onBlur | callback when component lose focus | Function() | |
|
||||
| onChange | callback when select value | Function(value: number) | |
|
||||
| onFocus | callback when component get focus | Function() | |
|
||||
| onHoverChange | callback when hover item | Function(value: number) | |
|
||||
| onKeyDown | callback when keydown on component | Function(event) | |
|
||||
| Property | Description | type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| allowClear | whether to allow clear when click again | boolean | true | |
|
||||
| allowHalf | whether to allow semi selection | boolean | false | |
|
||||
| autoFocus | get focus when component mounted | boolean | false | |
|
||||
| character | custom character of rate | ReactNode \| (RateProps) => ReactNode | [`<StarFilled />`](/components/icon/) | Function(): 4.4.0 |
|
||||
| className | custom class name of rate | string | | |
|
||||
| count | star count | number | 5 | |
|
||||
| defaultValue | default value | number | 0 | |
|
||||
| disabled | read only, unable to interact | boolean | false | |
|
||||
| style | custom style object of rate | CSSProperties | | |
|
||||
| tooltips | Customize tooltip by each character | string\[] | | |
|
||||
| value | current value | number | | |
|
||||
| onBlur | callback when component lose focus | Function() | | |
|
||||
| onChange | callback when select value | Function(value: number) | | |
|
||||
| onFocus | callback when component get focus | Function() | | |
|
||||
| onHoverChange | callback when hover item | Function(value: number) | | |
|
||||
| onKeyDown | callback when keydown on component | Function(event) | | |
|
||||
|
||||
## Methods
|
||||
|
||||
|
@ -15,24 +15,24 @@ cover: https://gw.alipayobjects.com/zos/alicdn/R5uiIWmxe/Rate.svg
|
||||
|
||||
## API
|
||||
|
||||
| 属性 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| allowClear | 是否允许再次点击后清除 | boolean | true |
|
||||
| allowHalf | 是否允许半选 | boolean | false |
|
||||
| autoFocus | 自动获取焦点 | boolean | false |
|
||||
| character | 自定义字符 | ReactNode | [`<StarFilled />`](/components/icon/) |
|
||||
| className | 自定义样式类名 | string | |
|
||||
| count | star 总数 | number | 5 |
|
||||
| defaultValue | 默认值 | number | 0 |
|
||||
| disabled | 只读,无法进行交互 | boolean | false |
|
||||
| style | 自定义样式对象 | CSSProperties | |
|
||||
| tooltips | 自定义每项的提示信息 | string\[] | |
|
||||
| value | 当前数,受控值 | number | |
|
||||
| onBlur | 失去焦点时的回调 | Function() | |
|
||||
| onChange | 选择时的回调 | Function(value: number) | |
|
||||
| onFocus | 获取焦点时的回调 | Function() | |
|
||||
| onHoverChange | 鼠标经过时数值变化的回调 | Function(value: number) | |
|
||||
| onKeyDown | 按键回调 | Function(event) | |
|
||||
| 属性 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| allowClear | 是否允许再次点击后清除 | boolean | true | |
|
||||
| allowHalf | 是否允许半选 | boolean | false | |
|
||||
| autoFocus | 自动获取焦点 | boolean | false | |
|
||||
| character | 自定义字符 | ReactNode \| (RateProps) => ReactNode | [`<StarFilled />`](/components/icon/) | Function(): 4.4.0 |
|
||||
| className | 自定义样式类名 | string | | |
|
||||
| count | star 总数 | number | 5 | |
|
||||
| defaultValue | 默认值 | number | 0 | |
|
||||
| disabled | 只读,无法进行交互 | boolean | false | |
|
||||
| style | 自定义样式对象 | CSSProperties | | |
|
||||
| tooltips | 自定义每项的提示信息 | string\[] | | |
|
||||
| value | 当前数,受控值 | number | | |
|
||||
| onBlur | 失去焦点时的回调 | Function() | | |
|
||||
| onChange | 选择时的回调 | Function(value: number) | | |
|
||||
| onFocus | 获取焦点时的回调 | Function() | | |
|
||||
| onHoverChange | 鼠标经过时数值变化的回调 | Function(value: number) | | |
|
||||
| onKeyDown | 按键回调 | Function(event) | | |
|
||||
|
||||
## 方法
|
||||
|
||||
|
@ -1108,6 +1108,26 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
|
||||
>
|
||||
Please check and modify the following information before resubmitting.
|
||||
</div>
|
||||
<div
|
||||
class="ant-result-extra"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Go Console
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Buy Again
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-result-content"
|
||||
>
|
||||
@ -1190,26 +1210,6 @@ exports[`renders ./components/result/demo/error.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-result-extra"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Go Console
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Buy Again
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -104,8 +104,8 @@ const Result: ResultType = props => (
|
||||
{renderIcon(prefixCls, props)}
|
||||
<div className={`${prefixCls}-title`}>{title}</div>
|
||||
{subTitle && <div className={`${prefixCls}-subtitle`}>{subTitle}</div>}
|
||||
{children && <div className={`${prefixCls}-content`}>{children}</div>}
|
||||
{renderExtra(prefixCls, props)}
|
||||
{children && <div className={`${prefixCls}-content`}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -741,7 +741,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
|
||||
gold
|
||||
<span
|
||||
aria-label="close"
|
||||
class="anticon anticon-close"
|
||||
class="anticon anticon-close ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
@ -770,7 +770,7 @@ exports[`renders ./components/select/demo/custom-tag-render.md correctly 1`] = `
|
||||
cyan
|
||||
<span
|
||||
aria-label="close"
|
||||
class="anticon anticon-close"
|
||||
class="anticon anticon-close ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
@ -23,7 +23,7 @@ const SkeletonAvatar = (props: AvatarProps) => {
|
||||
);
|
||||
};
|
||||
return <ConfigConsumer>{renderSkeletonAvatar}</ConfigConsumer>;
|
||||
}
|
||||
};
|
||||
|
||||
SkeletonAvatar.defaultProps = {
|
||||
size: 'default',
|
||||
|
@ -27,11 +27,12 @@ const Element = (props: SkeletonElementProps) => {
|
||||
const sizeStyle: React.CSSProperties =
|
||||
typeof size === 'number'
|
||||
? {
|
||||
width: size,
|
||||
height: size,
|
||||
lineHeight: `${size}px`,
|
||||
}
|
||||
width: size,
|
||||
height: size,
|
||||
lineHeight: `${size}px`,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(prefixCls, className, sizeCls, shapeCls)}
|
||||
@ -40,5 +41,4 @@ const Element = (props: SkeletonElementProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Element;
|
||||
|
35
components/skeleton/Image.tsx
Normal file
35
components/skeleton/Image.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { SkeletonElementProps } from './Element';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
|
||||
export interface SkeletonImageProps
|
||||
extends Omit<SkeletonElementProps, 'size' | 'shape' | 'active'> {}
|
||||
|
||||
const path =
|
||||
'M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z';
|
||||
|
||||
const SkeletonImage = (props: SkeletonImageProps) => {
|
||||
const renderSkeletonImage = ({ getPrefixCls }: ConfigConsumerProps) => {
|
||||
const { prefixCls: customizePrefixCls, className, style } = props;
|
||||
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
|
||||
const cls = classNames(prefixCls, className, `${prefixCls}-element`);
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
<div className={classNames(`${prefixCls}-image`, className)} style={style}>
|
||||
<svg
|
||||
viewBox="0 0 1098 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${prefixCls}-image-svg`}
|
||||
>
|
||||
<path d={path} className={`${prefixCls}-image-path`} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <ConfigConsumer>{renderSkeletonImage}</ConfigConsumer>;
|
||||
};
|
||||
|
||||
export default SkeletonImage;
|
@ -7,10 +7,10 @@ import Element from './Element';
|
||||
import SkeletonAvatar, { AvatarProps } from './Avatar';
|
||||
import SkeletonButton from './Button';
|
||||
import SkeletonInput from './Input';
|
||||
import SkeletonImage from './Image';
|
||||
|
||||
/* This only for skeleton internal. */
|
||||
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {
|
||||
}
|
||||
interface SkeletonAvatarProps extends Omit<AvatarProps, 'active'> {}
|
||||
|
||||
export interface SkeletonProps {
|
||||
active?: boolean;
|
||||
@ -170,5 +170,6 @@ Skeleton.defaultProps = {
|
||||
Skeleton.Button = SkeletonButton;
|
||||
Skeleton.Avatar = SkeletonAvatar;
|
||||
Skeleton.Input = SkeletonInput;
|
||||
Skeleton.Image = SkeletonImage;
|
||||
|
||||
export default Skeleton;
|
||||
|
@ -686,6 +686,27 @@ exports[`renders ./components/skeleton/demo/element.md correctly 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<div
|
||||
class="ant-skeleton-image"
|
||||
>
|
||||
<svg
|
||||
class="ant-skeleton-image-svg"
|
||||
viewBox="0 0 1098 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="ant-skeleton-image-path"
|
||||
d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -310,6 +310,27 @@ exports[`Skeleton button element size 3`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton image element should render normal 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element"
|
||||
>
|
||||
<div
|
||||
class="ant-skeleton-image"
|
||||
>
|
||||
<svg
|
||||
class="ant-skeleton-image-svg"
|
||||
viewBox="0 0 1098 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
class="ant-skeleton-image-path"
|
||||
d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Skeleton input element active 1`] = `
|
||||
<div
|
||||
class="ant-skeleton ant-skeleton-element ant-skeleton-active"
|
||||
|
@ -14,6 +14,7 @@ describe('Skeleton', () => {
|
||||
const genSkeletonButton = props => mount(<Skeleton.Button {...props} />);
|
||||
const genSkeletonAvatar = props => mount(<Skeleton.Avatar {...props} />);
|
||||
const genSkeletonInput = props => mount(<Skeleton.Input {...props} />);
|
||||
const genSkeletonImage = props => mount(<Skeleton.Image {...props} />);
|
||||
|
||||
mountTest(Skeleton);
|
||||
rtlTest(Skeleton);
|
||||
@ -31,7 +32,7 @@ describe('Skeleton', () => {
|
||||
it('should round title and paragraph', () => {
|
||||
const wrapperSmall = genSkeleton({ round: true, title: true, paragraph: true });
|
||||
expect(wrapperSmall.render()).toMatchSnapshot();
|
||||
})
|
||||
});
|
||||
|
||||
describe('avatar', () => {
|
||||
it('size', () => {
|
||||
@ -135,4 +136,11 @@ describe('Skeleton', () => {
|
||||
expect(wrapperLarge.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('image element', () => {
|
||||
it('should render normal', () => {
|
||||
const wrapper = genSkeletonImage();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,17 +1,17 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 骨架按钮、头像和输入框。
|
||||
en-US: Skeleton button, avatar and input.
|
||||
zh-CN: 骨架按钮、头像、输入框和图像。
|
||||
en-US: Skeleton button, avatar, input and Image.
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
骨架按钮、头像和输入框。
|
||||
骨架按钮、头像、输入框和图像。
|
||||
|
||||
## en-US
|
||||
|
||||
Skeleton button, avatar and input.
|
||||
Skeleton button, avatar, input and Image.
|
||||
|
||||
```jsx
|
||||
import { Skeleton, Switch, Form, Radio } from 'antd';
|
||||
@ -113,6 +113,10 @@ class Demo extends React.Component {
|
||||
</Form>
|
||||
<Skeleton.Input style={{ width: '300px' }} active={inputActive} size={inputSize} />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<Skeleton.Image />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
@skeleton-paragraph-prefix-cls: ~'@{skeleton-prefix-cls}-paragraph';
|
||||
@skeleton-button-prefix-cls: ~'@{skeleton-prefix-cls}-button';
|
||||
@skeleton-input-prefix-cls: ~'@{skeleton-prefix-cls}-input';
|
||||
@skeleton-image-prefix-cls: ~'@{skeleton-prefix-cls}-image';
|
||||
|
||||
.@{skeleton-prefix-cls} {
|
||||
display: table;
|
||||
@ -99,6 +100,10 @@
|
||||
.@{skeleton-input-prefix-cls} {
|
||||
.skeleton-color();
|
||||
}
|
||||
|
||||
.@{skeleton-image-prefix-cls} {
|
||||
.skeleton-color();
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton element
|
||||
@ -115,6 +120,10 @@
|
||||
.@{skeleton-input-prefix-cls} {
|
||||
.skeleton-element-input();
|
||||
}
|
||||
|
||||
.@{skeleton-image-prefix-cls} {
|
||||
.skeleton-element-image();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Button
|
||||
@ -168,6 +177,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Image
|
||||
.skeleton-element-image() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: top;
|
||||
background: @skeleton-color;
|
||||
|
||||
.skeleton-element-image-size(@image-size-base*2);
|
||||
|
||||
&-path {
|
||||
fill: #bfbfbf;
|
||||
}
|
||||
|
||||
&-svg {
|
||||
.skeleton-element-image-size(@image-size-base);
|
||||
max-width: @image-size-base * 4;
|
||||
max-height: @image-size-base * 4;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-element-avatar-size(@size) {
|
||||
width: @size;
|
||||
.skeleton-element-common-size(@size);
|
||||
@ -196,6 +226,15 @@
|
||||
.skeleton-element-common-size(@size);
|
||||
}
|
||||
|
||||
.skeleton-element-image-size(@size) {
|
||||
width: @size;
|
||||
.skeleton-element-common-size(@size);
|
||||
|
||||
&.@{skeleton-image-prefix-cls}-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-element-common-size(@size) {
|
||||
height: @size;
|
||||
line-height: @size;
|
||||
|
@ -47,9 +47,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&-content {
|
||||
width: @steps-desciption-max-width;
|
||||
}
|
||||
&-process .@{steps-prefix-cls}-item-icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
@ -186,7 +186,7 @@
|
||||
|
||||
// Modal
|
||||
// --
|
||||
@modal-header-padding: 11px @padding-lg;
|
||||
@modal-header-padding: 11px @modal-header-padding-horizontal;
|
||||
@modal-footer-padding-vertical: @padding-sm;
|
||||
@modal-header-close-size: 44px;
|
||||
@modal-confirm-body-padding: 24px 24px 16px;
|
||||
@ -292,3 +292,8 @@
|
||||
// Progress
|
||||
// ---
|
||||
@progress-circle-text-font-size: 0.833333em;
|
||||
|
||||
// Image
|
||||
// ---
|
||||
@image-size-base: 48px;
|
||||
@image-font-size-base: 24px;
|
||||
|
@ -490,9 +490,15 @@
|
||||
|
||||
// Modal
|
||||
// --
|
||||
@modal-header-padding-vertical: @padding-md;
|
||||
@modal-header-padding-horizontal: @padding-lg;
|
||||
@modal-body-padding: @padding-lg;
|
||||
@modal-header-bg: @component-background;
|
||||
@modal-header-padding: @padding-md @padding-lg;
|
||||
@modal-header-padding: @modal-header-padding-vertical @modal-header-padding-horizontal;
|
||||
@modal-header-border-width: @border-width-base;
|
||||
@modal-header-border-style: @border-style-base;
|
||||
@modal-header-title-line-height: 22px;
|
||||
@modal-header-title-font-size: @font-size-lg;
|
||||
@modal-header-border-color-split: @border-color-split;
|
||||
@modal-header-close-size: 56px;
|
||||
@modal-content-bg: @component-background;
|
||||
@ -500,8 +506,10 @@
|
||||
@modal-close-color: @text-color-secondary;
|
||||
@modal-footer-bg: transparent;
|
||||
@modal-footer-border-color-split: @border-color-split;
|
||||
@modal-footer-border-style: @border-style-base;
|
||||
@modal-footer-padding-vertical: 10px;
|
||||
@modal-footer-padding-horizontal: 16px;
|
||||
@modal-footer-border-width: @border-width-base;
|
||||
@modal-mask-bg: fade(@black, 45%);
|
||||
@modal-confirm-body-padding: 32px 32px 24px;
|
||||
|
||||
@ -966,4 +974,11 @@
|
||||
@result-title-font-size: 24px;
|
||||
@result-subtitle-font-size: @font-size-base;
|
||||
@result-icon-font-size: 72px;
|
||||
@result-extra-margin: 32px 0 0 0;
|
||||
@result-extra-margin: 24px 0 0 0;
|
||||
|
||||
// Image
|
||||
// ---
|
||||
@image-size-base: 48px;
|
||||
@image-font-size-base: 24px;
|
||||
@image-bg: #ccc;
|
||||
@image-color: #fff;
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
TablePaginationConfig,
|
||||
SortOrder,
|
||||
TableLocale,
|
||||
TableAction,
|
||||
} from './interface';
|
||||
import useSelection, { SELECTION_ALL, SELECTION_INVERT } from './hooks/useSelection';
|
||||
import useSorter, { getSortData, SortState } from './hooks/useSorter';
|
||||
@ -184,7 +185,11 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
||||
// ============================ Events =============================
|
||||
const changeEventInfo: Partial<ChangeEventInfo<RecordType>> = {};
|
||||
|
||||
const triggerOnChange = (info: Partial<ChangeEventInfo<RecordType>>, reset: boolean = false) => {
|
||||
const triggerOnChange = (
|
||||
info: Partial<ChangeEventInfo<RecordType>>,
|
||||
action: TableAction,
|
||||
reset: boolean = false,
|
||||
) => {
|
||||
const changeInfo = {
|
||||
...changeEventInfo,
|
||||
...info,
|
||||
@ -211,12 +216,18 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(changeInfo.pagination!, changeInfo.filters!, changeInfo.sorter!, {
|
||||
currentDataSource: getFilterData(
|
||||
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
|
||||
changeInfo.filterStates!,
|
||||
),
|
||||
});
|
||||
onChange(
|
||||
changeInfo.pagination!,
|
||||
changeInfo.filters!,
|
||||
changeInfo.sorter!,
|
||||
{
|
||||
currentDataSource: getFilterData(
|
||||
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
|
||||
changeInfo.filterStates!,
|
||||
),
|
||||
action,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -237,6 +248,7 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
||||
sorter,
|
||||
sorterStates,
|
||||
},
|
||||
'sort',
|
||||
false,
|
||||
);
|
||||
};
|
||||
@ -266,6 +278,7 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
||||
filters,
|
||||
filterStates,
|
||||
},
|
||||
'filter',
|
||||
true,
|
||||
);
|
||||
};
|
||||
@ -294,9 +307,12 @@ function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
||||
|
||||
// ========================== Pagination ==========================
|
||||
const onPaginationChange = (current: number, pageSize: number) => {
|
||||
triggerOnChange({
|
||||
pagination: { ...changeEventInfo.pagination, current, pageSize },
|
||||
});
|
||||
triggerOnChange(
|
||||
{
|
||||
pagination: { ...changeEventInfo.pagination, current, pageSize },
|
||||
},
|
||||
'paginate',
|
||||
);
|
||||
};
|
||||
|
||||
const [mergedPagination, resetPagination] = usePagination(
|
||||
|
@ -375,6 +375,7 @@ describe('Table.filter', () => {
|
||||
{},
|
||||
{
|
||||
currentDataSource: [],
|
||||
action: 'filter',
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -954,6 +955,7 @@ describe('Table.filter', () => {
|
||||
{},
|
||||
{
|
||||
currentDataSource: [],
|
||||
action: 'filter',
|
||||
},
|
||||
);
|
||||
expect(wrapper.find('.ant-pagination-item')).toHaveLength(0);
|
||||
@ -985,6 +987,7 @@ describe('Table.filter', () => {
|
||||
{},
|
||||
{
|
||||
currentDataSource: [],
|
||||
action: 'filter',
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -1057,7 +1060,10 @@ describe('Table.filter', () => {
|
||||
title: 'Name',
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
{
|
||||
currentDataSource: expect.anything(),
|
||||
action: 'sort',
|
||||
},
|
||||
);
|
||||
|
||||
// Filter it
|
||||
@ -1077,7 +1083,10 @@ describe('Table.filter', () => {
|
||||
title: 'Name',
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
{
|
||||
currentDataSource: expect.anything(),
|
||||
action: 'filter',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -100,7 +100,7 @@ describe('Table.pagination', () => {
|
||||
|
||||
wrapper.find('.ant-select-selector').simulate('mousedown');
|
||||
wrapper.find('.ant-select-item').last().simulate('click');
|
||||
expect(scrollTo).toHaveBeenCalledTimes(2);
|
||||
expect(scrollTo).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('fires change event', () => {
|
||||
@ -131,6 +131,7 @@ describe('Table.pagination', () => {
|
||||
{ key: 2, name: 'Tom' },
|
||||
{ key: 3, name: 'Jerry' },
|
||||
],
|
||||
action: 'paginate',
|
||||
},
|
||||
);
|
||||
|
||||
@ -179,6 +180,27 @@ describe('Table.pagination', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/24913
|
||||
it('should onChange called when pageSize change', () => {
|
||||
const onChange = jest.fn();
|
||||
const onShowSizeChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
createTable({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 200,
|
||||
onChange,
|
||||
onShowSizeChange,
|
||||
},
|
||||
}),
|
||||
);
|
||||
wrapper.find('.ant-select-selector').simulate('mousedown');
|
||||
expect(wrapper.find('.ant-select-item-option').length).toBe(4);
|
||||
wrapper.find('.ant-select-item-option').at(1).simulate('click');
|
||||
expect(onChange).toHaveBeenCalledWith(1, 20);
|
||||
});
|
||||
|
||||
it('should not change page when pagination current is specified', () => {
|
||||
const wrapper = mount(createTable({ pagination: { current: 2, pageSize: 1 } }));
|
||||
expect(wrapper.find('.ant-pagination-item-2').hasClass('ant-pagination-item-active')).toBe(
|
||||
@ -244,6 +266,7 @@ describe('Table.pagination', () => {
|
||||
{ key: 2, name: 'Tom' },
|
||||
{ key: 3, name: 'Jerry' },
|
||||
],
|
||||
action: 'paginate',
|
||||
},
|
||||
);
|
||||
expect(onPaginationChange).toHaveBeenCalledWith(2, 10);
|
||||
|
@ -787,27 +787,56 @@ describe('Table.rowSelection', () => {
|
||||
expect(onChange.mock.calls[0][1]).toEqual([expect.objectContaining({ name: 'bamboo' })]);
|
||||
});
|
||||
|
||||
it('do not cache selected keys', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Table
|
||||
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
|
||||
rowSelection={{ onChange }}
|
||||
rowKey="name"
|
||||
/>,
|
||||
);
|
||||
describe('cache with selected keys', () => {
|
||||
it('default not cache', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Table
|
||||
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
|
||||
rowSelection={{ onChange }}
|
||||
rowKey="name"
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
|
||||
|
||||
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
|
||||
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(['bamboo'], [{ name: 'bamboo' }]);
|
||||
});
|
||||
|
||||
it('cache with preserveSelectedRowKeys', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Table
|
||||
dataSource={[{ name: 'light' }, { name: 'bamboo' }]}
|
||||
rowSelection={{ onChange, preserveSelectedRowKeys: true }}
|
||||
rowKey="name"
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(['light'], [{ name: 'light' }]);
|
||||
|
||||
wrapper.setProps({ dataSource: [{ name: 'bamboo' }] });
|
||||
wrapper
|
||||
.find('tbody input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
['light', 'bamboo'],
|
||||
[{ name: 'light' }, { name: 'bamboo' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,12 +11,16 @@ title:
|
||||
|
||||
另外,本例也展示了筛选排序功能如何交给服务端实现,列不需要指定具体的 `onFilter` 和 `sorter` 函数,而是在把筛选和排序的参数发到服务端来处理。
|
||||
|
||||
当使用 `rowSelection` 时,请设置 `rowSelection.preserveSelectedRowKeys` 属性以保留 `key`。
|
||||
|
||||
**注意,此示例使用 [模拟接口](https://randomuser.me),展示数据可能不准确,请打开网络面板查看请求。**
|
||||
|
||||
## en-US
|
||||
|
||||
This example shows how to fetch and present data from a remote server, and how to implement filtering and sorting in server side by sending related parameters to server.
|
||||
|
||||
Setting `rowSelection.preserveSelectedRowKeys` to keep the `key` when enable selection.
|
||||
|
||||
**Note, this example use [Mock API](https://randomuser.me) that you can look up in Network Console.**
|
||||
|
||||
```jsx
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user