fix(input-number): many fixes (#259)

* fix(input-number): many fixes

- Fix `n-input-number` lacks `on-update-value` prop.
- Fix `n-input-number`'s value can't be null.
- Fix `n-input-number`'s button doesn't work after value is cleared, closes [#251](https://github.com/TuSimple/naive-ui/issues/251).
- `n-input-number` will focus directly, closes [#244](https://github.com/TuSimple/naive-ui/issues/244).

* docs(input-number): fixes

* docs
This commit is contained in:
07akioni 2021-06-23 13:35:14 +08:00 committed by GitHub
parent 6edf9db812
commit 4ebecee456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 61 deletions

View File

@ -14,7 +14,14 @@
### Fixes ### Fixes
- `n-select` can't input in filterable mode in single mode in iOS Safari, closes [#230](https://github.com/TuSimple/naive-ui/issues/230) - Fix `n-select` can't input in filterable mode in single mode in iOS Safari, closes [#230](https://github.com/TuSimple/naive-ui/issues/230)
- Fix `n-input-number` lacks `on-update-value` prop.
- Fix `n-input-number`'s value can't be null.
- Fix `n-input-number`'s button doesn't work after value is cleared, closes [#251](https://github.com/TuSimple/naive-ui/issues/251).
## Refactors
- `n-input-number` will focus directly, closes [#244](https://github.com/TuSimple/naive-ui/issues/244).
## 2.13.0 (2021-06-21) ## 2.13.0 (2021-06-21)

View File

@ -14,7 +14,14 @@
### Fixes ### Fixes
- `n-select` 在可过滤单选模式下在 iOS Safari 无法输入,关闭 [#230](https://github.com/TuSimple/naive-ui/issues/230) - 修复 `n-select` 在可过滤单选模式下在 iOS Safari 无法输入,关闭 [#230](https://github.com/TuSimple/naive-ui/issues/230)
- 修复 `n-input-number` 缺少 `on-update-value` 属性
- 修复 `n-input-number` 值无法为 `null`
- 修复 `n-input-number` 的按钮在值清空后无法使用,关闭 [#251](https://github.com/TuSimple/naive-ui/issues/251)
## Refactors
- `n-input-number` 会直接聚焦,关闭 [#244](https://github.com/TuSimple/naive-ui/issues/244)
## 2.13.0 (2021-06-21) ## 2.13.0 (2021-06-21)

View File

@ -29,7 +29,7 @@ show-button
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | The size of input box. | | size | `'small' \| 'medium' \| 'large'` | `'medium'` | The size of input box. |
| step | `number` | `1` | The number to which the current value is increased or decreased. It can be an integer or decimal. | | step | `number` | `1` | The number to which the current value is increased or decreased. It can be an integer or decimal. |
| validator | `(value) => boolean` | `undefined` | Setup custom validation. | | validator | `(value) => boolean` | `undefined` | Setup custom validation. |
| value | `number` | `undefined` | Value in controlled mode. | | value | `number \| null` | `undefined` | Value in controlled mode. |
| on-blur | `(event: FocusEvent) => void` | `undefined` | Callback when blur. | | on-blur | `(event: FocusEvent) => void` | `undefined` | Callback when blur. |
| on-focus | `(event: FocusEvent) => void` | `undefined` | Callback when focused. | | on-focus | `(event: FocusEvent) => void` | `undefined` | Callback when focused. |
| on-update:value | `(value: number) => void` | `undefined` | Callback when the component's value changes. | | on-update:value | `(value: number \| null) => void` | `undefined` | Callback when the component's value changes. |

View File

@ -0,0 +1,33 @@
# Debug
```html
<n-input-number
v-model:value="value1"
placeholder="最小值"
:min="-3"
:max="5"
@update:value="handleUpdateValue1"
/>
{{ JSON.stringify(value1) }}
<n-input-number v-model:value="value2" @update:value="handleUpdateValue2" />
{{ JSON.stringify(value2) }}
```
```js
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
handleUpdateValue1 (v) {
console.log(v)
},
handleUpdateValue2 (v) {
console.log(v)
},
value1: ref(null),
value2: ref(null)
}
}
})
```

View File

@ -13,6 +13,7 @@ size
step step
validator validator
show-button show-button
debug
``` ```
## Props ## Props
@ -32,4 +33,4 @@ show-button
| value | `number \| null` | `undefined` | 受控模式下的值 | | value | `number \| null` | `undefined` | 受控模式下的值 |
| on-blur | `(event: FocusEvent) => void` | `undefined` | 移除焦点的回调 | | on-blur | `(event: FocusEvent) => void` | `undefined` | 移除焦点的回调 |
| on-focus | `(event: FocusEvent) => void` | `undefined` | 获取焦点的回调 | | on-focus | `(event: FocusEvent) => void` | `undefined` | 获取焦点的回调 |
| on-update:value | `(value: number) => void` | `undefined` | 组件值发生变化的回调 | | on-update:value | `(value: number \| null) => void` | `undefined` | 组件值发生变化的回调 |

View File

@ -11,6 +11,7 @@ import type { ThemeProps } from '../../_mixins'
import { warn, call, MaybeArray, ExtractPublicPropTypes } from '../../_utils' import { warn, call, MaybeArray, ExtractPublicPropTypes } from '../../_utils'
import { inputNumberLight, InputNumberTheme } from '../styles' import { inputNumberLight, InputNumberTheme } from '../styles'
import { parse, validator, format, parseNumber } from './utils' import { parse, validator, format, parseNumber } from './utils'
import type { OnUpdateValue } from './interface'
const inputNumberProps = { const inputNumberProps = {
...(useTheme.props as ThemeProps<InputNumberTheme>), ...(useTheme.props as ThemeProps<InputNumberTheme>),
@ -19,27 +20,15 @@ const inputNumberProps = {
type: Number as PropType<number | null>, type: Number as PropType<number | null>,
default: null default: null
}, },
value: { value: Number,
type: Number,
default: undefined
},
step: { step: {
type: [Number, String], type: [Number, String],
default: 1 default: 1
}, },
min: { min: [Number, String],
type: [Number, String], max: [Number, String],
default: undefined
},
max: {
type: [Number, String],
default: undefined
},
size: String as PropType<'small' | 'medium' | 'large'>, size: String as PropType<'small' | 'medium' | 'large'>,
disabled: { disabled: Boolean,
type: Boolean,
default: false
},
validator: Function as PropType<(value: number) => boolean>, validator: Function as PropType<(value: number) => boolean>,
bordered: { bordered: {
type: Boolean as PropType<boolean | undefined>, type: Boolean as PropType<boolean | undefined>,
@ -49,17 +38,13 @@ const inputNumberProps = {
type: Boolean, type: Boolean,
default: true default: true
}, },
// eslint-disable-next-line vue/prop-name-casing 'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
'onUpdate:value': [Function, Array] as PropType< onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
MaybeArray<(value: number) => void>
>,
onFocus: [Function, Array] as PropType<MaybeArray<(e: FocusEvent) => void>>, onFocus: [Function, Array] as PropType<MaybeArray<(e: FocusEvent) => void>>,
onBlur: [Function, Array] as PropType<MaybeArray<(e: FocusEvent) => void>>, onBlur: [Function, Array] as PropType<MaybeArray<(e: FocusEvent) => void>>,
// deprecated // deprecated
onChange: { onChange: {
type: [Function, Array] as PropType< type: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
MaybeArray<(value: number) => void> | undefined
>,
validator: () => { validator: () => {
if (__DEV__) { if (__DEV__) {
warn( warn(
@ -124,39 +109,47 @@ export default defineComponent({
if (parsedNumber !== null) return parsedNumber if (parsedNumber !== null) return parsedNumber
else return null else return null
}) })
const doUpdateValue = (value: number): void => { const doUpdateValue = (value: number | null): void => {
const { value: mergedValue } = mergedValueRef const { value: mergedValue } = mergedValueRef
if (value === mergedValue) return if (value === mergedValue) return
const { 'onUpdate:value': onUpdateValue, onChange } = props const {
'onUpdate:value': _onUpdateValue,
onUpdateValue,
onChange
} = props
const { nTriggerFormInput, nTriggerFormChange } = formItem const { nTriggerFormInput, nTriggerFormChange } = formItem
if (onChange) call(onChange, value) if (onChange) call(onChange, value)
if (onUpdateValue) call(onUpdateValue, value) if (onUpdateValue) call(onUpdateValue, value)
if (_onUpdateValue) call(_onUpdateValue, value)
uncontrolledValueRef.value = value uncontrolledValueRef.value = value
nTriggerFormInput() nTriggerFormInput()
nTriggerFormChange() nTriggerFormChange()
} }
const deriveValueFromDisplayedValue = ( const deriveValueFromDisplayedValue = (
offset = 0, offset = 0,
postUpdateIfValid = true doUpdateIfValid = true
): null | number | false => { ): null | number | false => {
const { value: displayedValue } = displayedValueRef const { value: displayedValue } = displayedValueRef
const parsedValue = parse(displayedValue) const parsedValue = parse(displayedValue)
if (parsedValue === null) return null if (parsedValue === null) {
if (doUpdateIfValid) doUpdateValue(null)
return null
}
if (validator(parsedValue)) { if (validator(parsedValue)) {
let nextValue = parsedValue + offset let nextValue = parsedValue + offset
if (validator(nextValue)) { if (validator(nextValue)) {
const { value: mergedMax } = mergedMaxRef const { value: mergedMax } = mergedMaxRef
const { value: mergedMin } = mergedMinRef const { value: mergedMin } = mergedMinRef
if (mergedMax !== null && nextValue > mergedMax) { if (mergedMax !== null && nextValue > mergedMax) {
if (!postUpdateIfValid) return false if (!doUpdateIfValid) return false
nextValue = mergedMax nextValue = mergedMax
} }
if (mergedMin !== null && nextValue < mergedMin) { if (mergedMin !== null && nextValue < mergedMin) {
if (!postUpdateIfValid) return false if (!doUpdateIfValid) return false
nextValue = mergedMin nextValue = mergedMin
} }
if (props.validator && !props.validator(nextValue)) return false if (props.validator && !props.validator(nextValue)) return false
if (postUpdateIfValid) doUpdateValue(nextValue) if (doUpdateIfValid) doUpdateValue(nextValue)
return nextValue return nextValue
} }
} }
@ -307,6 +300,7 @@ export default defineComponent({
} }
function handleUpdateDisplayedValue (value: string): void { function handleUpdateDisplayedValue (value: string): void {
displayedValueRef.value = value displayedValueRef.value = value
deriveValueFromDisplayedValue()
} }
watch(mergedValueRef, () => { watch(mergedValueRef, () => {
deriveDisplayedValueFromValue() deriveDisplayedValueFromValue()
@ -360,7 +354,6 @@ export default defineComponent({
bordered={this.mergedBordered} bordered={this.mergedBordered}
value={this.displayedValue} value={this.displayedValue}
onUpdateValue={this.handleUpdateDisplayedValue} onUpdateValue={this.handleUpdateDisplayedValue}
passively-activated
theme={this.mergedTheme.peers.Input} theme={this.mergedTheme.peers.Input}
themeOverrides={this.mergedTheme.peerOverrides.Input} themeOverrides={this.mergedTheme.peerOverrides.Input}
builtinThemeOverrides={this.inputThemeOverrides} builtinThemeOverrides={this.inputThemeOverrides}

View File

@ -0,0 +1 @@
export type OnUpdateValue = (value: number | null) => void

View File

@ -1,17 +0,0 @@
import { mount } from '@vue/test-utils'
import { NInputNumber } from '../index'
import { NButton } from '../../button'
describe('n-input-number', () => {
it('should work with import on demand', () => {
mount(NInputNumber)
})
it('should work with `show-button` prop', async () => {
const wrapper = mount(NInputNumber)
expect(wrapper.findComponent(NButton).exists()).toBe(true)
await wrapper.setProps({ showButton: false })
expect(wrapper.findComponent(NButton).exists()).toBe(false)
})
})

View File

@ -0,0 +1,58 @@
import { mount } from '@vue/test-utils'
import { NInputNumber } from '../index'
import { NButton } from '../../button'
describe('n-input-number', () => {
it('should work with import on demand', () => {
mount(NInputNumber)
})
it('should work with `show-button` prop', async () => {
const wrapper = mount(NInputNumber)
expect(wrapper.findComponent(NButton).exists()).toBe(true)
await wrapper.setProps({ showButton: false })
expect(wrapper.findComponent(NButton).exists()).toBe(false)
})
it('should work with default value', async () => {
const wrapper = mount(NInputNumber, {
props: {
defaultValue: 1
}
})
expect(wrapper.find('input').element.value).toEqual('1')
})
it('should not trigger update if value is same', async () => {
const onUpdateValue = jest.fn()
const wrapper = mount(NInputNumber, {
attachTo: document.body,
props: {
defaultValue: 1,
onUpdateValue
}
})
wrapper.find('input').element.value = ''
await wrapper.find('input').trigger('input')
expect(onUpdateValue).toHaveBeenCalledWith(null)
wrapper.unmount()
})
it('trigger focus & blur event', () => {
const onFocus = jest.fn()
const onBlur = jest.fn()
const wrapper = mount(NInputNumber, {
attachTo: document.body,
props: {
onFocus,
onBlur
}
})
wrapper.find('input').element.focus()
expect(onFocus).toHaveBeenCalledTimes(1)
wrapper.find('input').element.blur()
expect(onBlur).toHaveBeenCalledTimes(1)
wrapper.unmount()
})
})

View File

@ -1,8 +0,0 @@
import { mount } from '@vue/test-utils'
import { NInput } from '../index'
describe('n-input', () => {
it('should work with import on demand', () => {
mount(NInput)
})
})

View File

@ -0,0 +1,19 @@
import { mount } from '@vue/test-utils'
import { NInput } from '../index'
describe('n-input', () => {
it('should work with import on demand', () => {
mount(NInput)
})
it('should call input callbacks', async () => {
const onUpdateValue = jest.fn()
const wrapper = mount(NInput, {
props: {
onUpdateValue
}
})
wrapper.find('input').element.value = 'cool'
await wrapper.find('input').trigger('input')
expect(onUpdateValue).toHaveBeenCalledWith('cool')
})
})