Skip to content

Commit 5e2a46c

Browse files
committed
feat: support allowClear
1 parent fdde44f commit 5e2a46c

File tree

6 files changed

+245
-0
lines changed

6 files changed

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

0 commit comments

Comments
 (0)