Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@
}
}
}
&-clear-icon {
padding: 0;
font-size: 12px;
background: none;
border: none;
}
&-clear-icon-hidden {
display: none;
}

&-action-down {
transition: all 0.3s;
Expand Down
12 changes: 12 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ nav:
<td>false</td>
<td>Specifies that an InputNumber should automatically get focus when the page loads</td>
</tr>
<tr>
<td>allowClear</td>
<td>boolean | { clearValue: number | string } </td>
<td>false</td>
<td>If allow to remove InputNumber content with clear</td>
</tr>
<tr>
<td>readOnly</td>
<td>Boolean</td>
Expand Down Expand Up @@ -148,6 +154,12 @@ nav:
<td></td>
<td>Called when the user clicks the arrows on the keyboard or interface and when the mouse wheel is spun.</td>
</tr>
<tr>
<td>onClear</td>
<td>() => void</td>
<td></td>
<td>This event will be triggered when the user clicks the "Clear" button.</td>
</tr>
<tr>
<td>style</td>
<td>Object</td>
Expand Down
25 changes: 25 additions & 0 deletions docs/demo/allow-clear.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint no-console:0 */
import InputNumber from '@rc-component/input-number';
import React from 'react';
import '../../assets/index.less';

export default () => {
const [value, setValue] = React.useState<string | number>(100);

const onChange = (val: number) => {
setValue(val);
};

return (
<div style={{ margin: 10 }}>
<InputNumber
allowClear={{ clearValue: 1 }}
style={{ width: 200 }}
value={value}
onChange={onChange}
prefix="¥"
suffix="RMB"
/>
</div>
);
};
4 changes: 4 additions & 0 deletions docs/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ nav:

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

## allow-clear

<code src="./demo/allow-clear.tsx"></code>

## combination-key-format

<code src="./demo/combination-key-format.tsx"></code>
Expand Down
22 changes: 22 additions & 0 deletions src/InputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export interface InputNumberProps<T extends ValueType = ValueType>
controls?: boolean;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
allowClear?: boolean | { clearValue?: string | number };
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;

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

onClear?: () => void;
onInput?: (text: string) => void;
onChange?: (value: T | null) => void;
onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
Expand Down Expand Up @@ -156,6 +158,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
prefix,
suffix,
stringMode,
allowClear,

parser,
formatter,
Expand All @@ -166,6 +169,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
onInput,
onPressEnter,
onStep,
onClear,

// Mouse Events
onMouseDown,
Expand Down Expand Up @@ -708,6 +712,24 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
readOnly={readOnly}
{...restProps}
/>
{allowClear && (
<button
type="button"
tabIndex={-1}
aria-label="清除"
onClick={() => {
onClear?.();
const updatedValue = getMiniDecimal(typeof allowClear === 'object' ? allowClear.clearValue : undefined);
triggerValueUpdate(updatedValue, false);
}}
className={clsx(`${prefixCls}-clear-icon`, {
[`${prefixCls}-clear-icon-hidden`]: !(!disabled && !readOnly && !decimalValue.isEmpty()),
[`${prefixCls}-clear-icon-has-suffix`]: !!suffix,
})}
>
</button>
)}

{suffix !== undefined && (
<div className={clsx(`${prefixCls}-suffix`, classNames?.suffix)} style={styles?.suffix}>
Expand Down
174 changes: 174 additions & 0 deletions tests/allowClear.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import InputNumber from '../src';

describe('InputNumber.AllowClear', () => {
it('should render clear icon when allowClear is true and value is not empty', () => {
const { container } = render(
<InputNumber allowClear value={123} />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).toBeTruthy();
expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议在此处增加一个测试用例,用于验证当 value0 时清除图标的可见性。这是一个重要的边界情况,因为 0 是一个假值(falsy value),可能会导致可见性判断出错。

你可以添加如下测试:

it('should render clear icon when value is 0', () => {
  const { container } = render(<InputNumber allowClear value={0} />);
  const clearIcon = container.querySelector('.rc-input-number-clear-icon');
  expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
});


it('should not render clear icon when value is empty', () => {
const { container } = render(
<InputNumber allowClear value={null} />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
});

it('should render clear icon when value is 0', () => {
const { container } = render(
<InputNumber allowClear value={0} />
);
const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
});

it('should not render clear icon when disabled', () => {
const { container } = render(
<InputNumber allowClear value={123} disabled />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
});

it('should not render clear icon when readOnly', () => {
const { container } = render(
<InputNumber allowClear value={123} readOnly />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-hidden');
});

it('should clear value to null when allowClear is true (boolean type)', () => {
const onChange = jest.fn();
const { container } = render(
<InputNumber allowClear value={123} onChange={onChange} />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
fireEvent.click(clearIcon);

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(null);
});

it('should clear value to custom value when allowClear is object with clearValue', () => {
const onChange = jest.fn();
const { container } = render(
<InputNumber
allowClear={{ clearValue: 0 }}
value={123}
onChange={onChange}
/>
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
fireEvent.click(clearIcon);

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(0);
});

it('should handle string clearValue correctly', () => {
const onChange = jest.fn();
const { container } = render(
<InputNumber
allowClear={{ clearValue: 'reset' }}
value={123}
onChange={onChange}
stringMode
/>
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
fireEvent.click(clearIcon);

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(null);
});

it('should support all clearValue types', () => {
const onChange1 = jest.fn();
const onChange2 = jest.fn();
const onChange3 = jest.fn();

// Test number clearValue
const { container: container1, unmount: unmount1 } = render(
<InputNumber allowClear={{ clearValue: 42 }} value={123} onChange={onChange1} />
);
fireEvent.click(container1.querySelector('.rc-input-number-clear-icon'));
expect(onChange1).toHaveBeenCalledWith(42);
unmount1();

// Test zero clearValue
const { container: container2, unmount: unmount2 } = render(
<InputNumber allowClear={{ clearValue: 0 }} value={123} onChange={onChange2} />
);
fireEvent.click(container2.querySelector('.rc-input-number-clear-icon'));
expect(onChange2).toHaveBeenCalledWith(0);
unmount2();

// Test undefined clearValue
const { container: container3, unmount: unmount3 } = render(
<InputNumber allowClear={{ clearValue: undefined }} value={123} onChange={onChange3} />
);
fireEvent.click(container3.querySelector('.rc-input-number-clear-icon'));
expect(onChange3).toHaveBeenCalledWith(null);
unmount3();
});

it('should trigger onClear callback when clear icon is clicked', () => {
const onClear = jest.fn();
const onChange = jest.fn();
const { container } = render(
<InputNumber
allowClear
value={123}
onClear={onClear}
onChange={onChange}
/>
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
fireEvent.click(clearIcon);

expect(onClear).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledTimes(1);
});

it('should have correct className when suffix is provided', () => {
const { container } = render(
<InputNumber allowClear value={123} suffix="$" />
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).toHaveClass('rc-input-number-clear-icon-has-suffix');
});

it('should work with defaultValue', () => {
const onChange = jest.fn();
const { container } = render(
<InputNumber
allowClear
defaultValue={100}
onChange={onChange}
/>
);

const clearIcon = container.querySelector('.rc-input-number-clear-icon');
expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
fireEvent.click(clearIcon);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个针对 defaultValue(非受控模式)的测试用例缺少了对清除按钮初始可见性的断言。在点击之前,应该先验证按钮是可见的,以确保它在非受控模式下能正确显示。

Suggested change
fireEvent.click(clearIcon);
expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden');
fireEvent.click(clearIcon);


expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(null);
});
});