feat(components): [input-number] add :value-on-clear and make it nullable (#7724)

This commit is contained in:
Carter Li 2022-05-17 01:16:44 +08:00 committed by GitHub
parent 9ae57642db
commit 2577b06328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 157 additions and 58 deletions

View File

@ -81,21 +81,22 @@ input-number/controlled
## Attributes
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | ------------------------------------------------ | ------------------ | --------------- | ----------- |
| model-value / v-model | binding value | number / undefined | — | — |
| min | the minimum allowed value | number | — | `-Infinity` |
| max | the maximum allowed value | number | — | `Infinity` |
| step | incremental step | number | — | 1 |
| step-strictly | whether input value can only be multiple of step | boolean | — | false |
| precision | precision of input value | number | — | — |
| size | size of the component | string | large/small | default |
| disabled | whether the component is disabled | boolean | — | false |
| controls | whether to enable the control buttons | boolean | — | true |
| controls-position | position of the control buttons | string | right | - |
| name | same as `name` in native input | string | — | — |
| label | label text | string | — | — |
| placeholder | placeholder in input | string | - | - |
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | ------------------------------------------------ | ---------------------- | --------------- | ----------- |
| model-value / v-model | binding value | number / undefined | — | — |
| min | the minimum allowed value | number | — | `-Infinity` |
| max | the maximum allowed value | number | — | `Infinity` |
| step | incremental step | number | — | 1 |
| step-strictly | whether input value can only be multiple of step | boolean | — | false |
| precision | precision of input value | number | — | — |
| size | size of the component | string | large/small | default |
| disabled | whether the component is disabled | boolean | — | false |
| controls | whether to enable the control buttons | boolean | — | true |
| controls-position | position of the control buttons | string | right | - |
| name | same as `name` in native input | string | — | — |
| label | label text | string | — | — |
| placeholder | placeholder in input | string | - | - |
| value-on-clear | value should be set when input box is cleared | string / number / null | min/max | - |
## Events

View File

@ -74,7 +74,7 @@ describe('InputNumber.vue', () => {
await nextTick()
expect(wrapper.find('input').element.value).toEqual('')
expect(wrapper.find('input').element.getAttribute('aria-valuenow')).toEqual(
'NaN'
'null'
)
})
test('min', async () => {
@ -278,9 +278,41 @@ describe('InputNumber.vue', () => {
expect(wrapper.getComponent(InputNumber).emitted('focus')).toHaveLength(1)
})
test('clear', async () => {
test('clear with :value-on-clear="null"', async () => {
const wrapper = _mount({
template: '<el-input-number v-model="num" :min="1"/>',
template: '<el-input-number v-model="num" :min="1" :max="10"/>',
setup() {
const num = ref(2)
return {
num,
}
},
})
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(null)
elInput.increase()
await nextTick()
expect(wrapper.vm.num).toBe(1)
elInput.increase()
await nextTick()
expect(wrapper.vm.num).toBe(2)
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(null)
elInput.decrease()
await nextTick()
expect(wrapper.vm.num).toBe(1)
elInput.decrease()
await nextTick()
expect(wrapper.vm.num).toBe(1)
})
test('clear with value-on-clear="min"', async () => {
const wrapper = _mount({
template:
'<el-input-number v-model="num" value-on-clear="min" :min="1" :max="10"/>',
setup() {
const num = ref(2)
return {
@ -303,6 +335,58 @@ describe('InputNumber.vue', () => {
expect(wrapper.vm.num).toBe(1)
})
test('clear with value-on-clear="max"', async () => {
const wrapper = _mount({
template:
'<el-input-number v-model="num" value-on-clear="max" :min="1" :max="10"/>',
setup() {
const num = ref(2)
return {
num,
}
},
})
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(10)
elInput.increase()
await nextTick()
expect(wrapper.vm.num).toBe(10)
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(10)
elInput.decrease()
await nextTick()
expect(wrapper.vm.num).toBe(9)
})
test('clear with :value-on-clear="5"', async () => {
const wrapper = _mount({
template:
'<el-input-number v-model="num" :value-on-clear="5" :min="1" :max="10"/>',
setup() {
const num = ref(2)
return {
num,
}
},
})
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(5)
elInput.increase()
await nextTick()
expect(wrapper.vm.num).toBe(6)
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(5)
elInput.decrease()
await nextTick()
expect(wrapper.vm.num).toBe(4)
})
test('check increase and decrease button when modelValue not in [min, max]', async () => {
const wrapper = _mount({
template: `

View File

@ -1,3 +1,4 @@
import { isNil } from 'lodash-unified'
import { buildProps, isNumber } from '@element-plus/utils'
import { componentSizes } from '@element-plus/constants'
@ -42,6 +43,12 @@ export const inputNumberProps = buildProps({
default: '',
values: ['', 'right'],
},
valueOnClear: {
type: [String, Number, null],
validator: (val: 'min' | 'max' | number | null) =>
val === null || isNumber(val) || ['min', 'max'].includes(val),
default: null,
},
name: String,
label: String,
placeholder: String,
@ -56,7 +63,6 @@ export const inputNumberEmits = {
change: (prev: number | undefined, cur: number | undefined) => prev !== cur,
blur: (e: FocusEvent) => e instanceof FocusEvent,
focus: (e: FocusEvent) => e instanceof FocusEvent,
input: (val: number | undefined) => isNumber(val),
'update:modelValue': (val: number | undefined) =>
isNumber(val) || val === undefined,
input: (val: number | null | undefined) => isNumber(val) || isNil(val),
'update:modelValue': (val: number | undefined) => isNumber(val) || isNil(val),
}

View File

@ -68,6 +68,7 @@ import {
ref,
watch,
} from 'vue'
import { isNil } from 'lodash-unified'
import { ElIcon } from '@element-plus/components/icon'
import { RepeatClick } from '@element-plus/directives'
@ -79,14 +80,14 @@ import {
useSize,
} from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import { debugWarn, isNumber, isUndefined } from '@element-plus/utils'
import { debugWarn, isNumber, isString, isUndefined } from '@element-plus/utils'
import { ArrowDown, ArrowUp, Minus, Plus } from '@element-plus/icons-vue'
import { inputNumberEmits, inputNumberProps } from './input-number'
import type { ComponentPublicInstance } from 'vue'
interface IData {
currentValue: number | undefined
currentValue: number | null | undefined
userInput: null | number | string
}
@ -116,10 +117,14 @@ export default defineComponent({
const ns = useNamespace('input-number')
const minDisabled = computed(
() => ensurePrecision(props.modelValue, -1) < props.min
() =>
isNumber(props.modelValue) &&
ensurePrecision(props.modelValue, -1) < props.min
)
const maxDisabled = computed(
() => ensurePrecision(props.modelValue) > props.max
() =>
isNumber(props.modelValue) &&
ensurePrecision(props.modelValue) > props.max
)
const numPrecision = computed(() => {
@ -147,7 +152,8 @@ export default defineComponent({
if (data.userInput !== null) {
return data.userInput
}
let currentValue: number | string | undefined = data.currentValue
let currentValue: number | string | undefined | null = data.currentValue
if (isNil(currentValue)) return ''
if (isNumber(currentValue)) {
if (Number.isNaN(currentValue)) return ''
if (!isUndefined(props.precision)) {
@ -166,8 +172,8 @@ export default defineComponent({
}
return Number.parseFloat(`${Math.round(num * 10 ** pre) / 10 ** pre}`)
}
const getPrecision = (value: number | undefined) => {
if (isUndefined(value)) return 0
const getPrecision = (value: number | null | undefined) => {
if (isNil(value)) return 0
const valueString = value.toString()
const dotPosition = valueString.indexOf('.')
let precision = 0
@ -179,7 +185,6 @@ export default defineComponent({
const ensurePrecision = (val: number, coefficient: 1 | -1 = 1) => {
if (!isNumber(val)) return data.currentValue
// Solve the accuracy problem of JS decimal calculation by converting the value to integer.
val = isNumber(val) ? val : Number.NaN
return toPrecision(val + props.step * coefficient)
}
const increase = () => {
@ -195,35 +200,38 @@ export default defineComponent({
setCurrentValue(newVal)
}
const verifyValue = (
value: number | string | undefined,
value: number | string | null | undefined,
update?: boolean
): number | undefined => {
const { max, min, step, precision, stepStrictly } = props
): number | null | undefined => {
const { max, min, step, precision, stepStrictly, valueOnClear } = props
let newVal = Number(value)
if (value === null) {
newVal = Number.NaN
if (isNil(value) || Number.isNaN(newVal)) {
return null
}
if (!Number.isNaN(newVal)) {
if (stepStrictly) {
newVal = Math.round(newVal / step) * step
}
if (!isUndefined(precision)) {
newVal = toPrecision(newVal, precision)
}
if (newVal > max || newVal < min) {
newVal = newVal > max ? max : min
update && emit('update:modelValue', newVal)
if (value === '') {
if (valueOnClear === null) {
return null
}
newVal = isString(valueOnClear)
? { min, max }[valueOnClear]
: valueOnClear
}
if (stepStrictly) {
newVal = Math.round(newVal / step) * step
}
if (!isUndefined(precision)) {
newVal = toPrecision(newVal, precision)
}
if (newVal > max || newVal < min) {
newVal = newVal > max ? max : min
update && emit('update:modelValue', newVal)
}
return newVal
}
const setCurrentValue = (value: number | string | undefined) => {
const setCurrentValue = (value: number | string | null | undefined) => {
const oldVal = data.currentValue
let newVal = verifyValue(value)
const newVal = verifyValue(value)
if (oldVal === newVal) return
if (Number.isNaN(newVal)) {
newVal = undefined
}
data.userInput = null
emit('update:modelValue', newVal)
emit('input', newVal)
@ -262,22 +270,22 @@ export default defineComponent({
watch(
() => props.modelValue,
(value) => {
const newVal = verifyValue(value, true)
data.currentValue = newVal
data.currentValue = verifyValue(value, true)
data.userInput = null
},
{ immediate: true }
)
onMounted(() => {
const { min, max, modelValue } = props
const innerInput = input.value?.input as HTMLInputElement
innerInput.setAttribute('role', 'spinbutton')
if (Number.isFinite(props.max)) {
innerInput.setAttribute('aria-valuemax', String(props.max))
if (Number.isFinite(max)) {
innerInput.setAttribute('aria-valuemax', String(max))
} else {
innerInput.removeAttribute('aria-valuemax')
}
if (Number.isFinite(props.min)) {
innerInput.setAttribute('aria-valuemin', String(props.min))
if (Number.isFinite(min)) {
innerInput.setAttribute('aria-valuemin', String(min))
} else {
innerInput.removeAttribute('aria-valuemin')
}
@ -286,10 +294,10 @@ export default defineComponent({
'aria-disabled',
String(inputNumberDisabled.value)
)
if (!isNumber(props.modelValue)) {
let val: number | undefined = Number(props.modelValue)
if (!isNumber(modelValue) && modelValue != null) {
let val: number | null = Number(modelValue)
if (Number.isNaN(val)) {
val = undefined
val = null
}
emit('update:modelValue', val)
}