Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(VNumberInput): parsing min/max values #20790

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
73 changes: 73 additions & 0 deletions packages/vuetify/playgrounds/Playground.number.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref } from 'vue';

// References for testing various boundary conditions
const value1 = ref(20); // Test for max within range
const value2 = ref(Number.MAX_SAFE_INTEGER + 1); // Test for exceeding max value
const value3 = ref(Number.MAX_SAFE_INTEGER + 1); // Test for unparseable max value

const value4 = ref(2); // Test for min within range
const value5 = ref(Number.MIN_SAFE_INTEGER - 1); // Test for exceeding min value
const value6 = ref(Number.MIN_SAFE_INTEGER - 1); // Test for unparseable min value

// Constants for boundary values
const minValue = 5;
const maxValue = 15;
</script>

<template>
<v-app>
<v-container>
<!-- Test case: Max value within range -->
<v-number-input
class="max-within-range"
:max="maxValue"
v-model="value1"
/>

<!-- Test case: Max value exceeds safe integer -->
<v-number-input
class="max-out-of-range1"
:max="Number.MAX_SAFE_INTEGER + 2"
v-model="value2"
/>

<!-- Test case: Max value set to "Infinity" -->
<v-number-input
class="max-out-of-range2"
max="Infinity"
v-model="value3"
/>

<!-- Test case: Min value within range -->
<v-number-input
class="min-within-range"
:min="minValue"
v-model="value4"
/>

<!-- Test case: Min value below safe integer -->
<v-number-input
class="min-out-of-range1"
:min="Number.MIN_SAFE_INTEGER - 2"
v-model="value5"
/>

<!-- Test case: Min value set to "-Infinity" -->
<v-number-input
class="min-out-of-range2"
min="-Infinity"
v-model="value6"
/>

<!-- Testing VNumberInput with various styles, icons, and options -->
<v-number-input prepend-inner-icon="mdi-alert" reverse />
<v-number-input prepend-inner-icon="mdi-square" hide-input />
<v-number-input hide-input />
<v-number-input prepend-inner-icon="mdi-circle-outline" />
<v-select prepend-inner-icon="mdi-check" placeholder="Normal padding" />


</v-container>
</v-app>
</template>
22 changes: 15 additions & 7 deletions packages/vuetify/src/labs/VNumberInput/VNumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ const makeVNumberInputProps = propsFactory({
default: null,
},
min: {
type: Number,
type: [Number, String],
default: Number.MIN_SAFE_INTEGER,
},
max: {
type: Number,
type: [Number, String],
default: Number.MAX_SAFE_INTEGER,
},
step: {
Expand All @@ -71,6 +71,14 @@ export const VNumberInput = genericComponent<VNumberInputSlots>()({

setup (props, { slots }) {
const _model = useProxiedModel(props, 'modelValue')
const min = computed(() => Math.max(
Number.isFinite(
parseFloat(props.min)) ? parseFloat(props.min) : Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER)
)
const max = computed(
() => Math.min(Number.isFinite(
parseFloat(props.max)) ? parseFloat(props.max) : Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
)

const model = computed({
get: () => _model.value,
Expand All @@ -83,7 +91,7 @@ export const VNumberInput = genericComponent<VNumberInputSlots>()({
}

const value = Number(val)
if (!isNaN(value) && value <= props.max && value >= props.min) {
if (!isNaN(value) && value <= max.value && value >= min.value) {
_model.value = value
}
},
Expand All @@ -101,11 +109,11 @@ export const VNumberInput = genericComponent<VNumberInputSlots>()({

const canIncrease = computed(() => {
if (controlsDisabled.value) return false
return (model.value ?? 0) as number + props.step <= props.max
return (model.value ?? 0) as number + props.step <= max.value
})
const canDecrease = computed(() => {
if (controlsDisabled.value) return false
return (model.value ?? 0) as number - props.step >= props.min
return (model.value ?? 0) as number - props.step >= min.value
})

const controlVariant = computed(() => {
Expand All @@ -130,7 +138,7 @@ export const VNumberInput = genericComponent<VNumberInputSlots>()({
function toggleUpDown (increment = true) {
if (controlsDisabled.value) return
if (model.value == null) {
model.value = clamp(0, props.min, props.max)
model.value = clamp(0, min.value, max.value)
return
}

Expand Down Expand Up @@ -196,7 +204,7 @@ export const VNumberInput = genericComponent<VNumberInputSlots>()({
if (!vTextFieldRef.value) return
const inputText = vTextFieldRef.value.value
if (inputText && !isNaN(+inputText)) {
model.value = clamp(+(inputText), props.min, props.max)
model.value = clamp(+(inputText), min.value, max.value)
} else {
model.value = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { VForm } from '@/components/VForm'

// Utilities
import { render, screen, userEvent } from '@test'
import { ref } from 'vue'
import { nextTick, ref } from 'vue'

describe('VNumberInput', () => {
it.each([
Expand Down Expand Up @@ -155,27 +155,63 @@ describe('VNumberInput', () => {
})
})

describe('native number input quirks', () => {
it('should not bypass min', async () => {
const model = ref(1)
render(() =>
<VNumberInput min={ 5 } max={ 15 } v-model={ model.value } />
)
describe('boundary value handling', () => {
it('should respect max value and fallback to max safe integer if max is out of range or cannot be parsed', () => {
const value1 = ref(20)
const value2 = ref(Number.MAX_SAFE_INTEGER + 1)
const value3 = ref(Number.MAX_SAFE_INTEGER + 1)

render(() => (
<>
<VNumberInput max={ 15 } v-model={ value1.value } class="max-within-range" />
<VNumberInput max={ Number.MAX_SAFE_INTEGER + 2 } v-model={ value2.value } class="max-outof-range1" />
<VNumberInput max="Infinity" v-model={ value3.value } class="max-outof-range2" />
</>
))

nextTick(() => {
// clamping the native input can be read after next tick

expect(screen.getByCSS('.max-within-range input')).toHaveValue('15')
expect(value1.value).toBe(15)

expect(screen.getByCSS('.max-outof-range1 input')).toHaveValue(Number.MAX_SAFE_INTEGER.toString())
expect(value2.value).toBe(Number.MAX_SAFE_INTEGER)

expect.element(screen.getByCSS('input')).toHaveValue('5')
expect(model.value).toBe(5)
expect(screen.getByCSS('.max-outof-range2 input')).toHaveValue(Number.MAX_SAFE_INTEGER.toString())
expect(value3.value).toBe(Number.MAX_SAFE_INTEGER)
})
})

it('should not bypass max', () => {
const model = ref(20)
render(() =>
<VNumberInput min={ 5 } max={ 15 } v-model={ model.value } />
)
it('should respect min value and fallback to min safe integer if min is out of range or cannot be parsed', () => {
const value1 = ref(2)
const value2 = ref(Number.MIN_SAFE_INTEGER - 1)
const value3 = ref(Number.MIN_SAFE_INTEGER - 1)

expect.element(screen.getByCSS('input')).toHaveValue('15')
expect(model.value).toBe(15)
render(() => (
<>
<VNumberInput min={ 5 } v-model={ value1.value } class="min-range-within-range" />
<VNumberInput min={ Number.MIN_SAFE_INTEGER - 2 } v-model={ value2.value } class="min-range-fallback1" />
<VNumberInput min="-Infinity" v-model={ value3.value } class="min-range-fallback2" />
</>
))

nextTick(() => {
// clamping the native input can be read after next tick

expect(screen.getByCSS('.min-range-within-range input')).toHaveValue('5')
expect(value1.value).toBe(5)

expect(screen.getByCSS('.min-range-fallback1 input')).toHaveValue(Number.MIN_SAFE_INTEGER.toString())
expect(value2.value).toBe(Number.MIN_SAFE_INTEGER)

expect(screen.getByCSS('.min-range-fallback2 input')).toHaveValue(Number.MIN_SAFE_INTEGER.toString())
expect(value3.value).toBe(Number.MIN_SAFE_INTEGER)
})
})
})

describe('native number input quirks', () => {
it('supports decimal step', async () => {
const model = ref(0)
render(() => (
Expand Down