Skip to content

Commit d39228f

Browse files
committed
feat: support allowClear
1 parent fdde44f commit d39228f

File tree

6 files changed

+236
-0
lines changed

6 files changed

+236
-0
lines changed

assets/index.less

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@
9494
}
9595
}
9696
}
97+
&-clear-icon {
98+
padding: 0;
99+
font-size: 12px;
100+
background: none;
101+
border: none;
102+
}
103+
&-clear-icon-hidden {
104+
display: none;
105+
}
97106

98107
&-action-down {
99108
transition: all 0.3s;

docs/api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ nav:
7676
<td>false</td>
7777
<td>Specifies that an InputNumber should automatically get focus when the page loads</td>
7878
</tr>
79+
<tr>
80+
<td>allowClear</td>
81+
<td>boolean | { clearValue: number | string } </td>
82+
<td>false</td>
83+
<td>If allow to remove InputNumber content with clear</td>
84+
</tr>
7985
<tr>
8086
<td>readOnly</td>
8187
<td>Boolean</td>
@@ -148,6 +154,12 @@ nav:
148154
<td></td>
149155
<td>Called when the user clicks the arrows on the keyboard or interface and when the mouse wheel is spun.</td>
150156
</tr>
157+
<tr>
158+
<td>onClear</td>
159+
<td>() => void</td>
160+
<td></td>
161+
<td>This event will be triggered when the user clicks the "Clear" button.</td>
162+
</tr>
151163
<tr>
152164
<td>style</td>
153165
<td>Object</td>

docs/demo/allow-clear.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint no-console:0 */
2+
import InputNumber from '@rc-component/input-number';
3+
import React from 'react';
4+
import '../../assets/index.less';
5+
6+
export default () => {
7+
const [value, setValue] = React.useState<string | number>(100);
8+
9+
const onChange = (val: number) => {
10+
setValue(val);
11+
};
12+
13+
return (
14+
<div style={{ margin: 10 }}>
15+
<InputNumber
16+
allowClear={{ clearValue: 1 }}
17+
style={{ width: 200 }}
18+
value={value}
19+
onChange={onChange}
20+
prefix="¥"
21+
suffix="RMB"
22+
/>
23+
</div>
24+
);
25+
};

docs/example.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ nav:
1313

1414
<code src="./demo/prefix-suffix.tsx"></code>
1515

16+
## allow-clear
17+
18+
<code src="./demo/allow-clear.tsx"></code>
19+
1620
## combination-key-format
1721

1822
<code src="./demo/combination-key-format.tsx"></code>

src/InputNumber.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface InputNumberProps<T extends ValueType = ValueType>
9898
controls?: boolean;
9999
prefix?: React.ReactNode;
100100
suffix?: React.ReactNode;
101+
allowClear?: boolean | { clearValue?: string | number };
101102
classNames?: Partial<Record<SemanticName, string>>;
102103
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
103104

@@ -116,6 +117,7 @@ export interface InputNumberProps<T extends ValueType = ValueType>
116117
/** Syntactic sugar of `formatter`. Config decimal separator of display. */
117118
decimalSeparator?: string;
118119

120+
onClear?: () => void;
119121
onInput?: (text: string) => void;
120122
onChange?: (value: T | null) => void;
121123
onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
@@ -156,6 +158,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
156158
prefix,
157159
suffix,
158160
stringMode,
161+
allowClear,
159162

160163
parser,
161164
formatter,
@@ -166,6 +169,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
166169
onInput,
167170
onPressEnter,
168171
onStep,
172+
onClear,
169173

170174
// Mouse Events
171175
onMouseDown,
@@ -708,6 +712,23 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
708712
readOnly={readOnly}
709713
{...restProps}
710714
/>
715+
{allowClear && (
716+
<button
717+
type="button"
718+
tabIndex={-1}
719+
onClick={() => {
720+
onClear?.();
721+
const updatedValue = getMiniDecimal(typeof allowClear === 'object' ? allowClear.clearValue : undefined)
722+
triggerValueUpdate(updatedValue, false)
723+
}}
724+
className={clsx(`${prefixCls}-clear-icon`, {
725+
[`${prefixCls}-clear-icon-hidden`]: !(!disabled && !readOnly && value),
726+
[`${prefixCls}-clear-icon-has-suffix`]: !!suffix,
727+
})}
728+
>
729+
730+
</button>
731+
)}
711732

712733
{suffix !== undefined && (
713734
<div className={clsx(`${prefixCls}-suffix`, classNames?.suffix)} style={styles?.suffix}>

tests/allowClear.test.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as React from 'react';
2+
import { render, fireEvent } from '@testing-library/react';
3+
import InputNumber from '../src';
4+
5+
describe('InputNumber.AllowClear', () => {
6+
it('should render clear icon when allowClear is true and value is not empty', () => {
7+
const { container } = render(
8+
<InputNumber allowClear value={123} />
9+
);
10+
11+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
12+
expect(clearIcon).toBeTruthy();
13+
expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
14+
});
15+
16+
it('should not render clear icon when value is empty', () => {
17+
const { container } = render(
18+
<InputNumber allowClear value={null} />
19+
);
20+
21+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
22+
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
23+
});
24+
25+
it('should not render clear icon when disabled', () => {
26+
const { container } = render(
27+
<InputNumber allowClear value={123} disabled />
28+
);
29+
30+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
31+
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
32+
});
33+
34+
it('should not render clear icon when readOnly', () => {
35+
const { container } = render(
36+
<InputNumber allowClear value={123} readOnly />
37+
);
38+
39+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
40+
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
41+
});
42+
43+
it('should clear value to null when allowClear is true (boolean type)', () => {
44+
const onChange = jest.fn();
45+
const { container } = render(
46+
<InputNumber allowClear value={123} onChange={onChange} />
47+
);
48+
49+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
50+
fireEvent.click(clearIcon);
51+
52+
expect(onChange).toHaveBeenCalledTimes(1);
53+
expect(onChange).toHaveBeenCalledWith(null);
54+
});
55+
56+
it('should clear value to custom value when allowClear is object with clearValue', () => {
57+
const onChange = jest.fn();
58+
const { container } = render(
59+
<InputNumber
60+
allowClear={{ clearValue: 0 }}
61+
value={123}
62+
onChange={onChange}
63+
/>
64+
);
65+
66+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
67+
fireEvent.click(clearIcon);
68+
69+
expect(onChange).toHaveBeenCalledTimes(1);
70+
expect(onChange).toHaveBeenCalledWith(0);
71+
});
72+
73+
it('should handle string clearValue correctly', () => {
74+
const onChange = jest.fn();
75+
const { container } = render(
76+
<InputNumber
77+
allowClear={{ clearValue: 'reset' }}
78+
value={123}
79+
onChange={onChange}
80+
stringMode
81+
/>
82+
);
83+
84+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
85+
fireEvent.click(clearIcon);
86+
87+
expect(onChange).toHaveBeenCalledTimes(1);
88+
expect(onChange).toHaveBeenCalledWith(null);
89+
});
90+
91+
it('should support all clearValue types', () => {
92+
const onChange1 = jest.fn();
93+
const onChange2 = jest.fn();
94+
const onChange3 = jest.fn();
95+
96+
// Test number clearValue
97+
const { container: container1, unmount: unmount1 } = render(
98+
<InputNumber allowClear={{ clearValue: 42 }} value={123} onChange={onChange1} />
99+
);
100+
fireEvent.click(container1.querySelector('.rc-input-number-clear-icon'));
101+
expect(onChange1).toHaveBeenCalledWith(42);
102+
unmount1();
103+
104+
// Test zero clearValue
105+
const { container: container2, unmount: unmount2 } = render(
106+
<InputNumber allowClear={{ clearValue: 0 }} value={123} onChange={onChange2} />
107+
);
108+
fireEvent.click(container2.querySelector('.rc-input-number-clear-icon'));
109+
expect(onChange2).toHaveBeenCalledWith(0);
110+
unmount2();
111+
112+
// Test undefined clearValue
113+
const { container: container3, unmount: unmount3 } = render(
114+
<InputNumber allowClear={{ clearValue: undefined }} value={123} onChange={onChange3} />
115+
);
116+
fireEvent.click(container3.querySelector('.rc-input-number-clear-icon'));
117+
expect(onChange3).toHaveBeenCalledWith(null);
118+
unmount3();
119+
});
120+
121+
it('should trigger onClear callback when clear icon is clicked', () => {
122+
const onClear = jest.fn();
123+
const onChange = jest.fn();
124+
const { container } = render(
125+
<InputNumber
126+
allowClear
127+
value={123}
128+
onClear={onClear}
129+
onChange={onChange}
130+
/>
131+
);
132+
133+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
134+
fireEvent.click(clearIcon);
135+
136+
expect(onClear).toHaveBeenCalledTimes(1);
137+
expect(onChange).toHaveBeenCalledTimes(1);
138+
});
139+
140+
it('should have correct className when suffix is provided', () => {
141+
const { container } = render(
142+
<InputNumber allowClear value={123} suffix="$" />
143+
);
144+
145+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
146+
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-has-suffix');
147+
});
148+
149+
it('should work with defaultValue', () => {
150+
const onChange = jest.fn();
151+
const { container } = render(
152+
<InputNumber
153+
allowClear
154+
defaultValue={100}
155+
onChange={onChange}
156+
/>
157+
);
158+
159+
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
160+
fireEvent.click(clearIcon);
161+
162+
expect(onChange).toHaveBeenCalledTimes(1);
163+
expect(onChange).toHaveBeenCalledWith(null);
164+
});
165+
});

0 commit comments

Comments
 (0)