diff --git a/src/packages/inputnumber/__tests__/inputnumber.spec.tsx b/src/packages/inputnumber/__tests__/inputnumber.spec.tsx index 606fec2100..cff6e24306 100644 --- a/src/packages/inputnumber/__tests__/inputnumber.spec.tsx +++ b/src/packages/inputnumber/__tests__/inputnumber.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { render, fireEvent, waitFor } from '@testing-library/react' +import { render, fireEvent, waitFor, act } from '@testing-library/react' import '@testing-library/jest-dom' import { InputNumber } from '../inputnumber' @@ -9,7 +9,7 @@ test('should render modelValue', () => { expect(container.querySelector('input')?.value).toBe('12') }) -test('should add step 2 when trigger click plus button', () => { +test('should add step 2 when trigger click plus button', async () => { const overlimit = vi.fn() const add = vi.fn() const change = vi.fn() @@ -23,13 +23,16 @@ test('should add step 2 when trigger click plus button', () => { /> ) const iconPlus = container.querySelectorAll('.nut-icon-Plus')[0] - fireEvent.click(iconPlus) + await act(async () => { + fireEvent.click(iconPlus) + }) + expect(overlimit).not.toBeCalled() - expect(add).toBeCalled() + expect(add).toHaveBeenCalled() expect(change.mock.calls[0][0]).toBe(3) }) -test('should minis step 2 when trigger click minis button', () => { +test('should minis step 2 when trigger click minis button', async () => { const overlimit = vi.fn() const reduce = vi.fn() const change = vi.fn() @@ -43,13 +46,15 @@ test('should minis step 2 when trigger click minis button', () => { /> ) const iconMinus = container.querySelectorAll('.nut-icon-Minus')[0] - fireEvent.click(iconMinus) + await act(async () => { + fireEvent.click(iconMinus) + }) expect(overlimit).not.toBeCalled() expect(reduce).toBeCalled() expect(change.mock.calls[0][0]).toBe(1) }) -test('should render max props', () => { +test('should render max props', async () => { const overlimit = vi.fn() const add = vi.fn() const change = vi.fn() @@ -64,13 +69,15 @@ test('should render max props', () => { /> ) const iconPlus = container.querySelectorAll('.nut-icon-Plus')[0] - fireEvent.click(iconPlus) + await act(async () => { + fireEvent.click(iconPlus) + }) expect(overlimit).toBeCalled() expect(add).toBeCalled() expect(change).not.toBeCalled() }) -test('should render min props', () => { +test('should render min props', async () => { const overlimit = vi.fn() const reduce = vi.fn() const change = vi.fn() @@ -85,7 +92,9 @@ test('should render min props', () => { /> ) const iconMinus = container.querySelectorAll('.nut-icon-Minus')[0] - fireEvent.click(iconMinus) + await act(async () => { + fireEvent.click(iconMinus) + }) expect(overlimit).toBeCalled() expect(reduce).toBeCalled() expect(change).not.toBeCalled() @@ -104,23 +113,27 @@ test('should not trigger click when disabled props to be true', () => { expect(container.querySelector('input')?.value).toBe('1') }) -test('should not focus input when readOnly props to be true', () => { +test('should not focus input when readOnly props to be true', async () => { const focus = vi.fn() const { container } = render( ) const iconMinus = container.querySelectorAll('.nut-icon-Minus')[0] - fireEvent.click(iconMinus) + await act(async () => { + fireEvent.click(iconMinus) + }) expect(container.querySelector('input')?.value).toBe('1') expect(focus).not.toBeCalled() }) -test('should render decimal when step props to be 0.2', () => { +test('should render decimal when step props to be 0.2', async () => { const { container } = render( ) const iconPlus = container.querySelectorAll('.nut-icon-Plus')[0] - fireEvent.click(iconPlus) + await act(async () => { + fireEvent.click(iconPlus) + }) expect(container.querySelector('input')?.value).toBe('2.2') }) @@ -158,19 +171,15 @@ test('allowEmpty', () => { }) }) -test('should overlimit when input', () => { - const change = vi.fn() +test('should overlimit when input', async () => { const overlimit = vi.fn() const { container } = render( - + ) const input = container.querySelectorAll('input')[0] input.value = '200' - fireEvent.input(input) - expect(change).toBeCalled() + await act(async () => { + fireEvent.input(input) + }) + expect(overlimit).toBeCalled() }) diff --git a/src/packages/inputnumber/demos/h5/demo8.tsx b/src/packages/inputnumber/demos/h5/demo8.tsx index faa2bb50d2..4da62e02d2 100644 --- a/src/packages/inputnumber/demos/h5/demo8.tsx +++ b/src/packages/inputnumber/demos/h5/demo8.tsx @@ -6,23 +6,26 @@ const Demo8 = () => { const overlimit = (e: any) => { console.log('超出限制事件触发', e) } - const onChange = (value: string | number) => { + + const beforeChange = (value: number | string): Promise => { Toast.show({ icon: 'loading', content: '异步演示2秒后更改' }) - console.log('onChange', value) - setTimeout(() => { - setInputValue(Number(value)) - Toast.clear() - }, 2000) + + return new Promise((resolve) => { + setTimeout(() => { + Toast.clear() + resolve(true) + }, 500) + }) } + return ( setInputValue(Number(value))} onOverlimit={overlimit} - async /> ) diff --git a/src/packages/inputnumber/demos/taro/demo8.tsx b/src/packages/inputnumber/demos/taro/demo8.tsx index a308b3a3e8..311d283a46 100644 --- a/src/packages/inputnumber/demos/taro/demo8.tsx +++ b/src/packages/inputnumber/demos/taro/demo8.tsx @@ -3,38 +3,47 @@ import { Cell, InputNumber, Toast } from '@nutui/nutui-react-taro' const Demo8 = () => { const [inputValue, setInputValue] = useState(0) - const [show, SetShow] = useState(false) - const [toastMsg, SetToastMsg] = useState('') - const [toastType, SetToastType] = useState('text') + const [show, setShow] = useState(false) + const [toastMsg, setToastMsg] = useState('') + const [toastType, setToastType] = useState('text') const toastShow = (msg: any, type: string) => { - SetToastMsg(msg) - SetToastType(type) - SetShow(true) + setToastMsg(msg) + setToastType(type) + setShow(true) } const overlimit = (e: any) => { console.log('超出限制事件触发', e) } - const onChange = (value: string | number) => { + + const beforeChange = (value: number | string): Promise => { toastShow('异步演示 2 秒后更改', 'loading') - console.log('onChange', value) - setTimeout(() => { - setInputValue(Number(value)) - SetShow(false) - }, 2000) + + return new Promise((resolve) => { + setTimeout(() => { + setShow(false) + resolve(true) + }, 500) + }) } + return ( <> - + setInputValue(Number(value))} + onOverlimit={overlimit} + /> - { - SetShow(false) + setShow(false) }} /> diff --git a/src/packages/inputnumber/doc.en-US.md b/src/packages/inputnumber/doc.en-US.md index ece6edb5b7..7f905483b8 100644 --- a/src/packages/inputnumber/doc.en-US.md +++ b/src/packages/inputnumber/doc.en-US.md @@ -121,9 +121,9 @@ Asynchronous modification through `change` event and `model-value` | digits | Set reserved decimal places | `string` \| `number` | `0` | | disabled | Disable all features | `boolean` | `false` | | readOnly | Read only status disables input box operation behavior | `boolean` | `false` | -| async | Support for asynchronous modification | `boolean` | `false` | | select | Support deselect all text | `boolean` | `true` | | formatter | Specifies the format of the value displayed in the input box | `function(value: number \| string): string` | `-` | +| beforeChange`2.8.0` | Callback function before the input value changes, return false to prevent input, support returning Promise | `(value: number \| string) => boolean \| Promise` | `-` | | onPlus | Triggered when the Add button is clicked | `(e: MouseEvent) => void` | `-` | | onMinus | Triggered when the decrease button is clicked | `(e: MouseEvent) => void` | `-` | | onOverlimit | Triggered when an unavailable button is clicked | `(e: MouseEvent) => void` | `-` | @@ -139,16 +139,17 @@ The component provides the following CSS variables, which can be used to customi | Name | Description | Default Value | | --- | --- | --- | -| \--nutui-inputnumber-input-width | The width of the input in the number input box | `26px` | -| \--nutui-inputnumber-input-height | The height of the input in the number input box | `20px` | +| \--nutui-inputnumber-input-width | The width of the input in the number input box | `40px` | +| \--nutui-inputnumber-input-height | The height of the input in the number input box | `24px` | | \--nutui-inputnumber-input-background-color | The background color of the input in the number input box | `$color-background` | | \--nutui-inputnumber-input-font-color | The font size color of the input in the number input box | `$color-title` | -| \--nutui-inputnumber-input-font-size | The font size of the input in the number input box | `12px` | +| \--nutui-inputnumber-input-font-size | The font size of the input in the number input box | `14px` | | \--nutui-inputnumber-input-border | The border value of the input in the number input box | `0` | -| \--nutui-inputnumber-input-border-radius | The rounded corners of the input in the number input box | `4px` | -| \--nutui-inputnumber-input-margin | The rounded corners of the input in the number input box | `0px` | -| \--nutui-inputnumber-button-width | The width of the left and right buttons of the number input box | `20px` | -| \--nutui-inputnumber-button-height | The height of the left and right buttons of the number input box | `20px` | +| \--nutui-inputnumber-input-border-radius | The rounded corners of the input in the number input box | `6px` | +| \--nutui-inputnumber-input-margin | The rounded corners of the input in the number input box | `0` | +| \--nutui-inputnumber-button-width | The width of the left and right buttons of the number input box | `14px` | +| \--nutui-inputnumber-button-height | The height of the left and right buttons of the number input box | `16px` | +| \--nutui-inputnumber-button-border-radius | The rounded corners of the left and right buttons of the number input box | `30px` | | \--nutui-inputnumber-button-background-color | The background color of the left and right buttons of the number input box | `transparent` | | \--nutui-inputnumber-icon-color | The color of the icon in the number input box | `$color-text` | | \--nutui-inputnumber-icon-size | The size of the icon in the number input box | `8px` | diff --git a/src/packages/inputnumber/doc.md b/src/packages/inputnumber/doc.md index e46492d6b0..9ff881b819 100644 --- a/src/packages/inputnumber/doc.md +++ b/src/packages/inputnumber/doc.md @@ -121,9 +121,9 @@ import { InputNumber } from '@nutui/nutui-react' | digits | 设置保留的小数位 | `string` \| `number` | `0` | | disabled | 禁用所有功能 | `boolean` | `false` | | readOnly | 只读状态禁用输入框操作行为 | `boolean` | `false` | -| async | 支持异步修改 | `boolean` | `false` | | select | 支持取消文本全选中 | `boolean` | `true` | | formatter | 指定输入框展示值的格式 | `function(value: number \| string): string` | `-` | +| beforeChange`2.8.0` | 输入值变化前的回调函数,返回 false 可阻止输入,支持返回 Promise | `(value: number \| string) => boolean \| Promise` | `-` | | onPlus | 点击增加按钮时触发 | `(e: MouseEvent) => void` | `-` | | onMinus | 点击减少按钮时触发 | `(e: MouseEvent) => void` | `-` | | onOverlimit | 点击不可用的按钮时触发 | `(e: MouseEvent) => void` | `-` | @@ -139,16 +139,17 @@ import { InputNumber } from '@nutui/nutui-react' | 名称 | 说明 | 默认值 | | --- | --- | --- | -| \--nutui-inputnumber-input-width | 数字输入框中input的宽度 | `26px` | -| \--nutui-inputnumber-input-height | 数字输入框中input的高度 | `20px` | +| \--nutui-inputnumber-input-width | 数字输入框中input的宽度 | `40px` | +| \--nutui-inputnumber-input-height | 数字输入框中input的高度 | `24px` | | \--nutui-inputnumber-input-background-color | 数字输入框中input的背景颜色 | `$color-background` | | \--nutui-inputnumber-input-font-color | 数字输入框中input的字号颜色 | `$color-title` | -| \--nutui-inputnumber-input-font-size | 数字输入框中input的字号大小 | `12px` | +| \--nutui-inputnumber-input-font-size | 数字输入框中input的字号大小 | `14px` | | \--nutui-inputnumber-input-border | 数字输入框中input的border值 | `0` | -| \--nutui-inputnumber-input-border-radius | 数字输入框中input的圆角 | `4px` | -| \--nutui-inputnumber-input-margin | 数字输入框中input的margin值 | `0px` | -| \--nutui-inputnumber-button-width | 数字输入框左右按钮的宽度 | `20px` | -| \--nutui-inputnumber-button-height | 数字输入框左右按钮的高度 | `20px` | +| \--nutui-inputnumber-input-border-radius | 数字输入框中input的圆角 | `6px` | +| \--nutui-inputnumber-input-margin | 数字输入框中input的margin值 | `0` | +| \--nutui-inputnumber-button-width | 数字输入框左右按钮的宽度 | `14px` | +| \--nutui-inputnumber-button-height | 数字输入框左右按钮的高度 | `16px` | +| \--nutui-inputnumber-button-border-radius | 数字输入框左右按钮的圆角 | `30px` | | \--nutui-inputnumber-button-background-color | 数字输入框左右按钮的背景色 | `transparent` | | \--nutui-inputnumber-icon-color | 数字输入框中icon的颜色 | `$color-text` | | \--nutui-inputnumber-icon-size | 数字输入框中icon的大小 | `8px` | diff --git a/src/packages/inputnumber/doc.taro.md b/src/packages/inputnumber/doc.taro.md index 06a13a10b4..566b7f4f29 100644 --- a/src/packages/inputnumber/doc.taro.md +++ b/src/packages/inputnumber/doc.taro.md @@ -114,12 +114,12 @@ import { InputNumber } from '@nutui/nutui-react-taro' | digits | 设置保留的小数位 | `string` \| `number` | `0` | | disabled | 禁用所有功能 | `boolean` | `false` | | readOnly | 只读状态禁用输入框操作行为 | `boolean` | `false` | -| async | 支持异步修改 | `boolean` | `false` | | formatter | 指定输入框展示值的格式 | `function(value: number \| string): string` | `-` | -| onPlus | 点击增加按钮时触发 | `(e: MouseEvent) => void` | `-` | -| onMinus | 点击减少按钮时触发 | `(e: MouseEvent) => void` | `-` | -| onOverlimit | 点击不可用的按钮时触发 | `(e: MouseEvent) => void` | `-` | -| onChange | 值改变时触发 | `(param: string \| number, e: MouseEvent \| ChangeEvent) => void` | `-` | +| beforeChange | 输入值变化前的回调函数,返回 false 可阻止输入,支持返回 Promise | `(value: number \| string) => boolean \| Promise` | `-` | +| onPlus | 点击增加按钮时触发 | `(e: ITouchEvent) => void` | `-` | +| onMinus | 点击减少按钮时触发 | `(e: ITouchEvent) => void` | `-` | +| onOverlimit | 点击不可用的按钮时触发 | `(e: ITouchEvent \| ChangeEvent) => void` | `-` | +| onChange | 值改变时触发 | `(param: string \| number, e: ITouchEvent \| ChangeEvent) => void` | `-` | | onFocus | 输入框获得焦点时触发 | `(e: FocusEvent) => void` | `-` | | onBlur | 输入框失去焦点时触发 | `(e: ChangeEvent) => void` | `-` | @@ -131,16 +131,17 @@ import { InputNumber } from '@nutui/nutui-react-taro' | 名称 | 说明 | 默认值 | | --- | --- | --- | -| \--nutui-inputnumber-input-width | 数字输入框中input的宽度 | `26px` | -| \--nutui-inputnumber-input-height | 数字输入框中input的高度 | `20px` | +| \--nutui-inputnumber-input-width | 数字输入框中input的宽度 | `40px` | +| \--nutui-inputnumber-input-height | 数字输入框中input的高度 | `24px` | | \--nutui-inputnumber-input-background-color | 数字输入框中input的背景颜色 | `$color-background` | | \--nutui-inputnumber-input-font-color | 数字输入框中input的字号颜色 | `$color-title` | -| \--nutui-inputnumber-input-font-size | 数字输入框中input的字号大小 | `12px` | +| \--nutui-inputnumber-input-font-size | 数字输入框中input的字号大小 | `14px` | | \--nutui-inputnumber-input-border | 数字输入框中input的border值 | `0` | -| \--nutui-inputnumber-input-border-radius | 数字输入框中input的圆角 | `4px` | -| \--nutui-inputnumber-input-margin | 数字输入框中input的margin值 | `0px` | -| \--nutui-inputnumber-button-width | 数字输入框左右按钮的宽度 | `20px` | -| \--nutui-inputnumber-button-height | 数字输入框左右按钮的高度 | `20px` | +| \--nutui-inputnumber-input-border-radius | 数字输入框中input的圆角 | `6px` | +| \--nutui-inputnumber-input-margin | 数字输入框中input的margin值 | `0` | +| \--nutui-inputnumber-button-width | 数字输入框左右按钮的宽度 | `14px` | +| \--nutui-inputnumber-button-height | 数字输入框左右按钮的高度 | `16px` | +| \--nutui-inputnumber-button-border-radius | 数字输入框左右按钮的圆角 | `30px` | | \--nutui-inputnumber-button-background-color | 数字输入框左右按钮的背景色 | `transparent` | | \--nutui-inputnumber-icon-color | 数字输入框中icon的颜色 | `$color-text` | | \--nutui-inputnumber-icon-size | 数字输入框中icon的大小 | `8px` | diff --git a/src/packages/inputnumber/doc.zh-TW.md b/src/packages/inputnumber/doc.zh-TW.md index 952ba80050..e516b283f3 100644 --- a/src/packages/inputnumber/doc.zh-TW.md +++ b/src/packages/inputnumber/doc.zh-TW.md @@ -113,8 +113,9 @@ import { InputNumber } from '@nutui/nutui-react' | digits | 設置保留的小數位 | `string` \| `number` | `0` | | disabled | 禁用所有功能 | `boolean` | `false` | | readOnly | 只讀狀態禁用輸入框操作行為 | `boolean` | `false` | -| async | 支持異步修改 | `boolean` | `false` | +| select | 支持取消文本全选中 | `boolean` | `true` | | formatter | 指定輸入框展示值的格式 | `function(value: number \| string): string` | `-` | +| beforeChange`2.8.0` | 输入值变化前的回调函数,返回 false 可阻止输入,支持返回 Promise | `(value: number \| string) => boolean \| Promise` | `-` | | onPlus | 點擊增加按鈕時觸發 | `(e: MouseEvent) => void` | `-` | | onMinus | 點擊減少按鈕時觸發 | `(e: MouseEvent) => void` | `-` | | onOverlimit | 點擊不可用的按鈕時觸發 | `(e: MouseEvent) => void` | `-` | @@ -130,16 +131,17 @@ import { InputNumber } from '@nutui/nutui-react' | 名稱 | 說明 | 默認值 | | --- | --- | --- | -| \--nutui-inputnumber-input-width | 數字輸入框中input的寬度 | `26px` | -| \--nutui-inputnumber-input-height | 數字輸入框中input的高度 | `20px` | +| \--nutui-inputnumber-input-width | 數字輸入框中input的寬度 | `40px` | +| \--nutui-inputnumber-input-height | 數字輸入框中input的高度 | `24px` | | \--nutui-inputnumber-input-background-color | 數字輸入框中input的背景顏色 | `$color-background` | | \--nutui-inputnumber-input-font-color | 數字輸入框中input的字號顏色 | `$color-title` | -| \--nutui-inputnumber-input-font-size | 數字輸入框中input的字號大小 | `12px` | +| \--nutui-inputnumber-input-font-size | 數字輸入框中input的字號大小 | `14px` | | \--nutui-inputnumber-input-border | 數字輸入框中input的border值 | `0` | -| \--nutui-inputnumber-input-border-radius | 數字輸入框中input的圓角 | `4px` | -| \--nutui-inputnumber-input-margin | 數字輸入框中input的margin值 | `0px` | -| \--nutui-inputnumber-button-width | 數字輸入框左右按鈕的寬度 | `20px` | -| \--nutui-inputnumber-button-height | 數字輸入框左右按鈕的高度 | `20px` | +| \--nutui-inputnumber-input-border-radius | 數字輸入框中input的圓角 | `6px` | +| \--nutui-inputnumber-input-margin | 數字輸入框中input的margin值 | `0` | +| \--nutui-inputnumber-button-width | 數字輸入框左右按鈕的寬度 | `14px` | +| \--nutui-inputnumber-button-height | 數字輸入框左右按鈕的高度 | `16px` | +| \--nutui-inputnumber-button-border-radius | 數字輸入框左右按鈕的圓角 | `30px` | | \--nutui-inputnumber-button-background-color | 數字輸入框左右按鈕的背景色 | `transparent` | | \--nutui-inputnumber-icon-color | 數字輸入框中icon的顏色 | `$color-text` | | \--nutui-inputnumber-icon-size | 數字輸入框中icon的大小 | `8px` | diff --git a/src/packages/inputnumber/inputnumber.taro.tsx b/src/packages/inputnumber/inputnumber.taro.tsx index 00d8f22b2d..08c0751d21 100644 --- a/src/packages/inputnumber/inputnumber.taro.tsx +++ b/src/packages/inputnumber/inputnumber.taro.tsx @@ -1,11 +1,11 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react' -import classNames from 'classnames' -import { ITouchEvent, Text, View } from '@tarojs/components' import { Minus, Plus } from '@nutui/icons-react-taro' +import classNames from 'classnames' +import { ITouchEvent, View } from '@tarojs/components' import { usePropsValue } from '@/hooks/use-props-value' import { ComponentDefaults } from '@/utils/typings' -import { harmony } from '@/utils/platform-taro' -import { SimpleValue, TaroInputNumberProps } from '@/types' +import { bound } from '@/utils/bound' +import { TaroInputNumberProps } from '@/types' const defaultProps = { ...ComponentDefaults, @@ -14,11 +14,10 @@ const defaultProps = { allowEmpty: false, min: 1, max: 9999, - type: 'digit', step: 1, digits: 0, - async: false, select: true, + beforeChange: (value) => Promise.resolve(true), } as TaroInputNumberProps const classPrefix = `nut-inputnumber` @@ -38,7 +37,6 @@ export const InputNumber: FunctionComponent< allowEmpty, digits, step, - async, select, className, style, @@ -49,13 +47,15 @@ export const InputNumber: FunctionComponent< onBlur, onFocus, onChange, + beforeChange, ...restProps } = { ...defaultProps, ...props, } - const isHarmony = harmony() - const classes = classNames(classPrefix, className) + const classes = classNames(classPrefix, className, { + [`${classPrefix}-disabled`]: disabled, + }) const [focused, setFocused] = useState(false) const inputRef = useRef(null) useEffect(() => { @@ -64,32 +64,23 @@ export const InputNumber: FunctionComponent< } }, [select, focused]) - const [shadowValue, setShadowValue] = usePropsValue< - SimpleValue | undefined | null - >({ + const [shadowValue, setShadowValue] = usePropsValue({ value: typeof value === 'string' ? parseFloat(value) : value, defaultValue: typeof defaultValue === 'string' ? parseFloat(defaultValue) : defaultValue, finalValue: 0, - onChange: (value) => {}, }) - const bound = (value: number, min: number, max: number) => { - let res = value - if (min !== undefined) { - res = Math.max(Number(min), res) - } - if (max !== undefined) { - res = Math.min(Number(max), res) - } - return res - } - const format = (value: SimpleValue | undefined | null): string => { - if (value === null || value === undefined) return '' + + const format = (value: number | null | string): string => { + if (value === null) return '' // 如果超过 min 或 max, 需要纠正 - if (typeof value === 'string') value = parseFloat(value) - const fixedValue = bound(value as any, Number(min), Number(max)) + const fixedValue = bound( + typeof value === 'string' ? parseFloat(value) : value, + Number(min), + Number(max) + ) if (formatter) { return formatter(fixedValue) } @@ -98,57 +89,53 @@ export const InputNumber: FunctionComponent< } return fixedValue.toString() } + const [inputValue, setInputValue] = useState(format(shadowValue)) useEffect(() => { - if (!focused && !async) { - setShadowValue(bound(Number(shadowValue), Number(min), Number(max))) + if (!focused) { setInputValue(format(shadowValue)) } }, [focused, shadowValue]) - useEffect(() => { - if (async) { - setShadowValue(bound(Number(value), Number(min), Number(max))) - setInputValue(format(value)) - } - }, [value]) - - const calcNextValue = (current: any, step: any, symbol: number) => { + const calcNextValue = (current: any, stepValue: any, symbol: number) => { const dig = digits + 1 - return ( - (parseFloat(current || '0') * dig + parseFloat(step) * dig * symbol) / dig - ) + const currentValue = parseFloat(current || '0') + const stepAmount = parseFloat(stepValue) * symbol + return (currentValue * dig + stepAmount * dig) / dig } - const update = (negative: boolean, e: ITouchEvent) => { - if (step !== undefined) { - const shouldOverBoundary = calcNextValue( - shadowValue, - step, - negative ? -1 : 1 - ) - const nextValue = bound(shouldOverBoundary, Number(min), Number(max)) - setShadowValue(nextValue) - if ( - negative - ? shouldOverBoundary < Number(min) - : shouldOverBoundary > Number(max) - ) { - onOverlimit?.(e) - } else { - onChange?.(nextValue, e) - } + + const update = async (negative: boolean, e: ITouchEvent) => { + if (step === undefined) return + negative ? onMinus?.(e) : onPlus?.(e) + + const shouldOverBoundary = calcNextValue( + bound(Number(shadowValue), Number(min), Number(max)), + step, + negative ? -1 : 1 + ) + const maybeResume = await beforeChange(Number(shouldOverBoundary)) + if (!maybeResume) return + + const nextValue = bound(shouldOverBoundary, Number(min), Number(max)) + setShadowValue(nextValue) + if ( + negative + ? shouldOverBoundary < Number(min) + : shouldOverBoundary > Number(max) + ) { + onOverlimit?.(e) + } else { + onChange?.(nextValue, e) } } - const handleReduce = (e: ITouchEvent) => { + const handleReduce = async (e: ITouchEvent) => { if (disabled) return - onMinus?.(e) - update(true, e) + await update(true, e) } - const handlePlus = (e: ITouchEvent) => { + const handlePlus = async (e: ITouchEvent) => { if (disabled) return - onPlus?.(e) - update(false, e) + await update(false, e) } const parseValue = (text: string) => { @@ -156,40 +143,28 @@ export const InputNumber: FunctionComponent< if (text === '-') return null return text } - const clampValue = (valueStr: string | null) => { - if (valueStr === null) return defaultValue - const val = Number(parseFloat(valueStr || '0').toFixed(digits)) - return Math.max(Number(min), Math.min(Number(max), val)) - } - const handleValueChange = ( - valueStr: string | null, - e: React.ChangeEvent - ) => { - const val = clampValue(valueStr) - // input暂不触发onOverlimit - // if (val !== Number(e.target.value)) { - // onOverlimit?.(e) - // } - if (val !== Number(shadowValue) && val !== undefined) { - onChange?.(val, e) - } - } - const handleInputChange = (e: any) => { + const handleInputChange = async (e: React.ChangeEvent) => { // 设置 input 值, 在 blur 时格式化 setInputValue(e.target.value) const valueStr = parseValue(e.target.value) - if (valueStr === null) { - if (allowEmpty) { - setShadowValue(null) - } else { - setShadowValue(defaultValue) - } + const maybeResume = await beforeChange(Number(valueStr)) + if (!maybeResume) return + + setShadowValue( + // eslint-disable-next-line no-nested-ternary + valueStr === null ? (allowEmpty ? null : defaultValue) : valueStr + ) + + if ( + valueStr !== null && + (Number(valueStr) < Number(min) || Number(valueStr) > Number(max)) + ) { + onOverlimit?.(e) } else { - setShadowValue(clampValue(valueStr) as any) + onChange?.(parseFloat(valueStr || '0').toFixed(digits), e) } - !async && handleValueChange(valueStr, e) } - const handleFocus = (e: any) => { + const handleFocus = (e: React.FocusEvent) => { setFocused(true) setInputValue( shadowValue !== undefined && shadowValue !== null @@ -198,48 +173,25 @@ export const InputNumber: FunctionComponent< ) onFocus?.(e) } - const handleBlur = (e: any) => { + const handleBlur = (e: React.FocusEvent) => { setFocused(false) - onBlur?.(e) + onBlur && onBlur(e) const valueStr = parseValue(e.target.value) - if (valueStr === null) { - if (allowEmpty) { - setShadowValue(null) - } else { - setShadowValue(defaultValue) - } - } else { - setShadowValue(clampValue(valueStr) as any) - } - async && handleValueChange(valueStr, e) + onChange?.(parseFloat(valueStr || '0').toFixed(digits) as any, e) } return ( - {isHarmony ? ( - - - - - ) : ( - - )} + - {isHarmony ? ( - - + - - ) : ( - - )} + ) diff --git a/src/packages/inputnumber/inputnumber.tsx b/src/packages/inputnumber/inputnumber.tsx index 727d2c2bbf..4c09186e64 100644 --- a/src/packages/inputnumber/inputnumber.tsx +++ b/src/packages/inputnumber/inputnumber.tsx @@ -3,7 +3,8 @@ import { Minus, Plus } from '@nutui/icons-react' import classNames from 'classnames' import { usePropsValue } from '@/hooks/use-props-value' import { ComponentDefaults } from '@/utils/typings' -import { SimpleValue, WebInputNumberProps } from '@/types' +import { bound } from '@/utils/bound' +import { WebInputNumberProps } from '@/types' const defaultProps = { ...ComponentDefaults, @@ -14,8 +15,8 @@ const defaultProps = { max: 9999, step: 1, digits: 0, - async: false, select: true, + beforeChange: (value) => Promise.resolve(true), } as WebInputNumberProps const classPrefix = `nut-inputnumber` @@ -34,7 +35,6 @@ export const InputNumber: FunctionComponent< allowEmpty, digits, step, - async, select, className, style, @@ -45,6 +45,7 @@ export const InputNumber: FunctionComponent< onBlur, onFocus, onChange, + beforeChange, ...restProps } = { ...defaultProps, @@ -61,32 +62,23 @@ export const InputNumber: FunctionComponent< } }, [select, focused]) - const [shadowValue, setShadowValue] = usePropsValue< - SimpleValue | undefined | null - >({ + const [shadowValue, setShadowValue] = usePropsValue({ value: typeof value === 'string' ? parseFloat(value) : value, defaultValue: typeof defaultValue === 'string' ? parseFloat(defaultValue) : defaultValue, finalValue: 0, - onChange: (value) => {}, }) - const bound = (value: number, min: number, max: number) => { - let res = value - if (min !== undefined) { - res = Math.max(Number(min), res) - } - if (max !== undefined) { - res = Math.min(Number(max), res) - } - return res - } - const format = (value: SimpleValue | undefined | null): string => { - if (value === null || value === undefined) return '' + + const format = (value: number | null | string): string => { + if (value === null) return '' // 如果超过 min 或 max, 需要纠正 - if (typeof value === 'string') value = parseFloat(value) - const fixedValue = bound(value as any, Number(min), Number(max)) + const fixedValue = bound( + typeof value === 'string' ? parseFloat(value) : value, + Number(min), + Number(max) + ) if (formatter) { return formatter(fixedValue) } @@ -95,55 +87,53 @@ export const InputNumber: FunctionComponent< } return fixedValue.toString() } + const [inputValue, setInputValue] = useState(format(shadowValue)) useEffect(() => { - if (!focused && !async) { - setShadowValue(bound(Number(shadowValue), Number(min), Number(max))) + if (!focused) { setInputValue(format(shadowValue)) } }, [focused, shadowValue]) - useEffect(() => { - if (async) { - setShadowValue(bound(Number(value), Number(min), Number(max))) - setInputValue(format(value)) - } - }, [value]) - const calcNextValue = (current: any, step: any, symbol: number) => { + + const calcNextValue = (current: any, stepValue: any, symbol: number) => { const dig = digits + 1 - return ( - (parseFloat(current || '0') * dig + parseFloat(step) * dig * symbol) / dig - ) + const currentValue = parseFloat(current || '0') + const stepAmount = parseFloat(stepValue) * symbol + return (currentValue * dig + stepAmount * dig) / dig } - const update = (negative: boolean, e: React.MouseEvent) => { - if (step !== undefined) { - const shouldOverBoundary = calcNextValue( - shadowValue, - step, - negative ? -1 : 1 - ) - const nextValue = bound(shouldOverBoundary, Number(min), Number(max)) - setShadowValue(nextValue) - if ( - negative - ? shouldOverBoundary < Number(min) - : shouldOverBoundary > Number(max) - ) { - onOverlimit?.(e) - } else { - onChange?.(nextValue, e) - } + + const update = async (negative: boolean, e: React.MouseEvent) => { + if (step === undefined) return + negative ? onMinus?.(e) : onPlus?.(e) + + const shouldOverBoundary = calcNextValue( + bound(Number(shadowValue), Number(min), Number(max)), + step, + negative ? -1 : 1 + ) + const maybeResume = await beforeChange(Number(shouldOverBoundary)) + if (!maybeResume) return + + const nextValue = bound(shouldOverBoundary, Number(min), Number(max)) + setShadowValue(nextValue) + if ( + negative + ? shouldOverBoundary < Number(min) + : shouldOverBoundary > Number(max) + ) { + onOverlimit?.(e) + } else { + onChange?.(nextValue, e) } } - const handleReduce = (e: React.MouseEvent) => { + const handleReduce = async (e: React.MouseEvent) => { if (disabled) return - onMinus?.(e) - update(true, e) + await update(true, e) } - const handlePlus = (e: React.MouseEvent) => { + const handlePlus = async (e: React.MouseEvent) => { if (disabled) return - onPlus?.(e) - update(false, e) + await update(false, e) } const parseValue = (text: string) => { @@ -151,39 +141,26 @@ export const InputNumber: FunctionComponent< if (text === '-') return null return text } - const clampValue = (valueStr: string | null) => { - if (valueStr === null) return defaultValue - const val = Number(parseFloat(valueStr || '0').toFixed(digits)) - return Math.max(Number(min), Math.min(Number(max), val)) - } - - const handleValueChange = ( - valueStr: string | null, - e: React.ChangeEvent - ) => { - const val = clampValue(valueStr) - // input暂不触发onOverlimit - // if (val !== Number(e.target.value)) { - // onOverlimit?.(e) - // } - if (val !== Number(shadowValue) && val !== undefined) { - onChange?.(val, e) - } - } - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = async (e: React.ChangeEvent) => { // 设置 input 值, 在 blur 时格式化 setInputValue(e.target.value) const valueStr = parseValue(e.target.value) - if (valueStr === null) { - if (allowEmpty) { - setShadowValue(null) - } else { - setShadowValue(defaultValue) - } + const maybeResume = await beforeChange(Number(valueStr)) + if (!maybeResume) return + + setShadowValue( + // eslint-disable-next-line no-nested-ternary + valueStr === null ? (allowEmpty ? null : defaultValue) : valueStr + ) + + if ( + valueStr !== null && + (Number(valueStr) < Number(min) || Number(valueStr) > Number(max)) + ) { + onOverlimit?.(e) } else { - setShadowValue(clampValue(valueStr) as any) + onChange?.(parseFloat(valueStr || '0').toFixed(digits), e) } - !async && handleValueChange(valueStr, e) } const handleFocus = (e: React.FocusEvent) => { setFocused(true) @@ -196,18 +173,9 @@ export const InputNumber: FunctionComponent< } const handleBlur = (e: React.FocusEvent) => { setFocused(false) - onBlur?.(e) + onBlur && onBlur(e) const valueStr = parseValue(e.target.value) - if (valueStr === null) { - if (allowEmpty) { - setShadowValue(null) - } else { - setShadowValue(defaultValue) - } - } else { - setShadowValue(clampValue(valueStr) as any) - } - async && handleValueChange(valueStr, e) + onChange?.(parseFloat(valueStr || '0').toFixed(digits) as any, e) } return ( diff --git a/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md b/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md index 00fc017702..570aa4230b 100644 --- a/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md +++ b/src/sites/sites-react/doc/docs/react/migrate-from-v2.en-US.md @@ -157,7 +157,11 @@ If your project uses these components, please read the documentation carefully a [//]: # '#### Form' [//]: # '#### Form.Item' [//]: # '#### Input' - [//]: # '#### InputNumber' + +#### InputNumber + +- Remove async, which can be replaced by beforeChange +- Add beforeChange to handle asynchronous calls [//]: # '#### NumberKeyboard' [//]: # '#### Picker' [//]: # '#### Radio' diff --git a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md index cefa3450f9..a9cdf02f2f 100644 --- a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md @@ -158,7 +158,12 @@ plugins: [ [//]: # '#### Form' [//]: # '#### Form.Item' [//]: # '#### Input' -[//]: # '#### InputNumber' + +#### InputNumber + +- 移除 `async`, 可通过 `beforeChange` 替代 +- 增加 `beforeChange`, 处理异步调用 + [//]: # '#### NumberKeyboard' [//]: # '#### Picker' [//]: # '#### Radio' diff --git a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md index 00fc017702..570aa4230b 100644 --- a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md +++ b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.en-US.md @@ -157,7 +157,11 @@ If your project uses these components, please read the documentation carefully a [//]: # '#### Form' [//]: # '#### Form.Item' [//]: # '#### Input' - [//]: # '#### InputNumber' + +#### InputNumber + +- Remove async, which can be replaced by beforeChange +- Add beforeChange to handle asynchronous calls [//]: # '#### NumberKeyboard' [//]: # '#### Picker' [//]: # '#### Radio' diff --git a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md index 478c92e6e1..4d673d5814 100644 --- a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md @@ -157,7 +157,16 @@ plugins: [ [//]: # '#### Form' [//]: # '#### Form.Item' [//]: # '#### Input' - [//]: # '#### InputNumber' + +#### InputNumber + +- 移除 `async`, 可通过 `beforeChange` 替代 +- 增加 `beforeChange`, 处理异步调用 + [//]: # '#### NumberKeyboard' + [//]: # '#### Picker' + [//]: # '#### Radio' + [//]: # '### Radio.Group' + [//]: # '#### Range' [//]: # '#### NumberKeyboard' [//]: # '#### Picker' [//]: # '#### Radio' diff --git a/src/types/spec/inputnumber/base.ts b/src/types/spec/inputnumber/base.ts index 7102b00e36..f6930618cd 100644 --- a/src/types/spec/inputnumber/base.ts +++ b/src/types/spec/inputnumber/base.ts @@ -1,23 +1,23 @@ -import { SimpleValue } from '../../base/atoms' import { BaseProps } from '../../base/props' +import { SimpleValue } from '../../base/atoms' export interface BaseInputNumber extends BaseProps { - value?: SimpleValue - defaultValue?: SimpleValue + value: SimpleValue + defaultValue: SimpleValue + allowEmpty: boolean min: SimpleValue max: SimpleValue - allowEmpty: boolean disabled: boolean readOnly: boolean step: number digits: number - async: boolean select: boolean - formatter?: (value?: string | number) => string + formatter?: (value?: SimpleValue) => string onPlus: (e: any) => void onMinus: (e: any) => void onOverlimit: (e: any) => void onBlur: (e: any) => void onFocus: (e: any) => void + beforeChange: (value: SimpleValue) => boolean | Promise onChange: (param: string | number, e: any) => void }