mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-08 01:53:34 +08:00
feat: input support show count (#32522)
Co-authored-by: machixian <machixian@myweimai.com>
This commit is contained in:
parent
ec625a00db
commit
e0de0249b2
@ -16,6 +16,10 @@ export interface InputFocusOptions extends FocusOptions {
|
|||||||
cursor?: 'start' | 'end' | 'all';
|
cursor?: 'start' | 'end' | 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShowCountProps {
|
||||||
|
formatter: (args: { count: number; maxLength?: number }) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'type'> {
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'type'> {
|
||||||
prefixCls?: string;
|
prefixCls?: string;
|
||||||
@ -52,6 +56,7 @@ export interface InputProps
|
|||||||
prefix?: React.ReactNode;
|
prefix?: React.ReactNode;
|
||||||
suffix?: React.ReactNode;
|
suffix?: React.ReactNode;
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
|
showCount?: boolean | ShowCountProps;
|
||||||
bordered?: boolean;
|
bordered?: boolean;
|
||||||
htmlSize?: number;
|
htmlSize?: number;
|
||||||
}
|
}
|
||||||
@ -344,12 +349,46 @@ class Input extends React.Component<InputProps, InputState> {
|
|||||||
onKeyDown?.(e);
|
onKeyDown?.(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderShowCountSuffix = (prefixCls: string) => {
|
||||||
|
const { value } = this.state;
|
||||||
|
const { maxLength, suffix, showCount } = this.props;
|
||||||
|
// Max length value
|
||||||
|
const hasMaxLength = Number(maxLength) > 0;
|
||||||
|
|
||||||
|
if (suffix || showCount) {
|
||||||
|
const valueLength = [...fixControlledValue(value)].length;
|
||||||
|
let dataCount = null;
|
||||||
|
if (typeof showCount === 'object') {
|
||||||
|
dataCount = showCount.formatter({ count: valueLength, maxLength });
|
||||||
|
} else {
|
||||||
|
dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!showCount && (
|
||||||
|
<span
|
||||||
|
className={classNames(`${prefixCls}-show-count-suffix`, {
|
||||||
|
[`${prefixCls}-show-count-has-suffix`]: !!suffix,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{dataCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{suffix}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
renderComponent = ({ getPrefixCls, direction, input }: ConfigConsumerProps) => {
|
renderComponent = ({ getPrefixCls, direction, input }: ConfigConsumerProps) => {
|
||||||
const { value, focused } = this.state;
|
const { value, focused } = this.state;
|
||||||
const { prefixCls: customizePrefixCls, bordered = true } = this.props;
|
const { prefixCls: customizePrefixCls, bordered = true } = this.props;
|
||||||
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
||||||
this.direction = direction;
|
this.direction = direction;
|
||||||
|
|
||||||
|
const showCountSuffix = this.renderShowCountSuffix(prefixCls);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SizeContext.Consumer>
|
<SizeContext.Consumer>
|
||||||
{size => (
|
{size => (
|
||||||
@ -366,6 +405,7 @@ class Input extends React.Component<InputProps, InputState> {
|
|||||||
focused={focused}
|
focused={focused}
|
||||||
triggerFocus={this.focus}
|
triggerFocus={this.focus}
|
||||||
bordered={bordered}
|
bordered={bordered}
|
||||||
|
suffix={showCountSuffix}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SizeContext.Consumer>
|
</SizeContext.Consumer>
|
||||||
|
@ -3147,6 +3147,41 @@ Array [
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`renders ./components/input/demo/show-count.md correctly 1`] = `
|
||||||
|
Array [
|
||||||
|
<span
|
||||||
|
class="ant-input-affix-wrapper"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="ant-input"
|
||||||
|
maxlength="20"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="ant-input-suffix"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ant-input-show-count-suffix"
|
||||||
|
>
|
||||||
|
0 / 20
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>,
|
||||||
|
<br />,
|
||||||
|
<br />,
|
||||||
|
<div
|
||||||
|
class="ant-input-textarea ant-input-textarea-show-count"
|
||||||
|
data-count="0 / 100"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
class="ant-input"
|
||||||
|
maxlength="100"
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`renders ./components/input/demo/size.md correctly 1`] = `
|
exports[`renders ./components/input/demo/size.md correctly 1`] = `
|
||||||
Array [
|
Array [
|
||||||
<span
|
<span
|
||||||
@ -3310,18 +3345,6 @@ Array [
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders ./components/input/demo/textarea-show-count.md correctly 1`] = `
|
|
||||||
<div
|
|
||||||
class="ant-input-textarea ant-input-textarea-show-count"
|
|
||||||
data-count="0 / 100"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
class="ant-input"
|
|
||||||
maxlength="100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders ./components/input/demo/tooltip.md correctly 1`] = `
|
exports[`renders ./components/input/demo/tooltip.md correctly 1`] = `
|
||||||
<input
|
<input
|
||||||
class="ant-input"
|
class="ant-input"
|
||||||
|
@ -133,6 +133,49 @@ describe('As Form Control', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should support showCount', () => {
|
||||||
|
it('maxLength', () => {
|
||||||
|
const wrapper = mount(<Input maxLength={5} showCount value="12345" />);
|
||||||
|
expect(wrapper.find('input').prop('value')).toBe('12345');
|
||||||
|
expect(wrapper.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('5 / 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('control exceed maxLength', () => {
|
||||||
|
const wrapper = mount(<Input maxLength={5} showCount value="12345678" />);
|
||||||
|
expect(wrapper.find('input').prop('value')).toBe('12345678');
|
||||||
|
expect(wrapper.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('8 / 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emoji', () => {
|
||||||
|
it('should minimize value between emoji length and maxLength', () => {
|
||||||
|
const wrapper = mount(<Input maxLength={1} showCount value="👀" />);
|
||||||
|
expect(wrapper.find('input').prop('value')).toBe('👀');
|
||||||
|
expect(wrapper.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('1 / 1');
|
||||||
|
|
||||||
|
const wrapper1 = mount(<Input maxLength={2} showCount value="👀" />);
|
||||||
|
expect(wrapper1.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('1 / 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('slice emoji', () => {
|
||||||
|
const wrapper = mount(<Input maxLength={5} showCount value="1234😂" />);
|
||||||
|
expect(wrapper.find('input').prop('value')).toBe('1234😂');
|
||||||
|
expect(wrapper.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('5 / 5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('count formatter', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<Input
|
||||||
|
maxLength={5}
|
||||||
|
showCount={{ formatter: ({ count, maxLength }) => `${count}, ${maxLength}` }}
|
||||||
|
value="12345"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(wrapper.find('input').prop('value')).toBe('12345');
|
||||||
|
expect(wrapper.find('.ant-input-show-count-suffix').getDOMNode().innerHTML).toBe('5, 5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Input allowClear', () => {
|
describe('Input allowClear', () => {
|
||||||
it('should change type when click', () => {
|
it('should change type when click', () => {
|
||||||
const wrapper = mount(<Input allowClear />);
|
const wrapper = mount(<Input allowClear />);
|
||||||
|
34
components/input/demo/show-count.md
Normal file
34
components/input/demo/show-count.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
order: 12
|
||||||
|
title:
|
||||||
|
zh-CN: 带字数提示
|
||||||
|
en-US: With character counting
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
展示字数提示。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
Show character counting.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Input } from 'antd';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const onChange = e => {
|
||||||
|
console.log('Change:', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<>
|
||||||
|
<Input showCount maxLength={20} onChange={onChange} />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<TextArea showCount maxLength={100} onChange={onChange} />
|
||||||
|
</>,
|
||||||
|
mountNode,
|
||||||
|
);
|
||||||
|
```
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
order: 12
|
|
||||||
title:
|
|
||||||
zh-CN: 带字数提示的文本域
|
|
||||||
en-US: Textarea with character counting
|
|
||||||
---
|
|
||||||
|
|
||||||
## zh-CN
|
|
||||||
|
|
||||||
展示字数提示。
|
|
||||||
|
|
||||||
## en-US
|
|
||||||
|
|
||||||
Show character counting.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Input } from 'antd';
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
const onChange = e => {
|
|
||||||
console.log('Change:', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<TextArea showCount maxLength={100} onChange={onChange} />, mountNode);
|
|
||||||
```
|
|
@ -26,6 +26,7 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca
|
|||||||
| disabled | Whether the input is disabled | boolean | false | |
|
| disabled | Whether the input is disabled | boolean | false | |
|
||||||
| id | The ID for input | string | - | |
|
| id | The ID for input | string | - | |
|
||||||
| maxLength | The max length | number | - | |
|
| maxLength | The max length | number | - | |
|
||||||
|
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.17.0 |
|
||||||
| prefix | The prefix icon for the Input | ReactNode | - | |
|
| prefix | The prefix icon for the Input | ReactNode | - | |
|
||||||
| size | The size of the input box. Note: in the context of a form, the `large` size is used | `large` \| `middle` \| `small` | - | |
|
| size | The size of the input box. Note: in the context of a form, the `large` size is used | `large` \| `middle` \| `small` | - | |
|
||||||
| suffix | The suffix icon for the Input | ReactNode | - | |
|
| suffix | The suffix icon for the Input | ReactNode | - | |
|
||||||
|
@ -27,6 +27,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
|
|||||||
| disabled | 是否禁用状态,默认为 false | boolean | false | |
|
| disabled | 是否禁用状态,默认为 false | boolean | false | |
|
||||||
| id | 输入框的 id | string | - | |
|
| id | 输入框的 id | string | - | |
|
||||||
| maxLength | 最大长度 | number | - | |
|
| maxLength | 最大长度 | number | - | |
|
||||||
|
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.17.0 |
|
||||||
| prefix | 带有前缀图标的 input | ReactNode | - | |
|
| prefix | 带有前缀图标的 input | ReactNode | - | |
|
||||||
| size | 控件大小。注:标准表单内的输入框大小限制为 `large` | `large` \| `middle` \| `small` | - | |
|
| size | 控件大小。注:标准表单内的输入框大小限制为 `large` | `large` \| `middle` \| `small` | - | |
|
||||||
| suffix | 带有后缀图标的 input | ReactNode | - | |
|
| suffix | 带有后缀图标的 input | ReactNode | - | |
|
||||||
|
@ -52,6 +52,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-show-count-suffix {
|
||||||
|
color: @text-color-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-show-count-has-suffix {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&-prefix {
|
&-prefix {
|
||||||
margin-right: @input-affix-margin;
|
margin-right: @input-affix-margin;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user