diff --git a/assets/index.less b/assets/index.less index 08cebed8..7e5566d1 100644 --- a/assets/index.less +++ b/assets/index.less @@ -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; diff --git a/docs/api.md b/docs/api.md index 97406ba4..d173768a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -76,6 +76,12 @@ nav: false Specifies that an InputNumber should automatically get focus when the page loads + + allowClear + boolean | { clearValue: number | string } + false + If allow to remove InputNumber content with clear + readOnly Boolean @@ -148,6 +154,12 @@ nav: Called when the user clicks the arrows on the keyboard or interface and when the mouse wheel is spun. + + onClear + () => void + + This event will be triggered when the user clicks the "Clear" button. + style Object diff --git a/docs/demo/allow-clear.tsx b/docs/demo/allow-clear.tsx new file mode 100644 index 00000000..0b1ebf0f --- /dev/null +++ b/docs/demo/allow-clear.tsx @@ -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(100); + + const onChange = (val: number) => { + setValue(val); + }; + + return ( +
+ +
+ ); +}; diff --git a/docs/example.md b/docs/example.md index 7b2ec5d6..65821c38 100644 --- a/docs/example.md +++ b/docs/example.md @@ -13,6 +13,10 @@ nav: +## allow-clear + + + ## combination-key-format diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 7ffc2bdc..6febf07b 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -98,6 +98,7 @@ export interface InputNumberProps controls?: boolean; prefix?: React.ReactNode; suffix?: React.ReactNode; + allowClear?: boolean | { clearValue?: string | number }; classNames?: Partial>; styles?: Partial>; @@ -116,6 +117,7 @@ export interface InputNumberProps /** 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; @@ -156,6 +158,7 @@ const InputNumber = React.forwardRef((props, r prefix, suffix, stringMode, + allowClear, parser, formatter, @@ -166,6 +169,7 @@ const InputNumber = React.forwardRef((props, r onInput, onPressEnter, onStep, + onClear, // Mouse Events onMouseDown, @@ -708,6 +712,24 @@ const InputNumber = React.forwardRef((props, r readOnly={readOnly} {...restProps} /> + {allowClear && ( + + )} {suffix !== undefined && (
diff --git a/tests/allowClear.test.tsx b/tests/allowClear.test.tsx new file mode 100644 index 00000000..268464c3 --- /dev/null +++ b/tests/allowClear.test.tsx @@ -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( + + ); + + const clearIcon = container.querySelector('.rc-input-number-clear-icon'); + expect(clearIcon).toBeTruthy(); + expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden'); + }); + + it('should not render clear icon when value is empty', () => { + const { container } = render( + + ); + + 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( + + ); + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + fireEvent.click(container1.querySelector('.rc-input-number-clear-icon')); + expect(onChange1).toHaveBeenCalledWith(42); + unmount1(); + + // Test zero clearValue + const { container: container2, unmount: unmount2 } = render( + + ); + fireEvent.click(container2.querySelector('.rc-input-number-clear-icon')); + expect(onChange2).toHaveBeenCalledWith(0); + unmount2(); + + // Test undefined clearValue + const { container: container3, unmount: unmount3 } = render( + + ); + 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( + + ); + + 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( + + ); + + 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( + + ); + + const clearIcon = container.querySelector('.rc-input-number-clear-icon'); + expect(clearIcon).not.toHaveClass('rc-input-number-clear-icon-hidden'); + fireEvent.click(clearIcon); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(null); + }); +}); \ No newline at end of file