Skip to content

Commit

Permalink
fix: steps, min value, empty initial value
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed May 11, 2024
1 parent 3e3f96a commit 2372d82
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 30 deletions.
5 changes: 2 additions & 3 deletions packages/radix-vue/src/NumberField/NumberFieldDecrement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ onTrigger(() => {
rootContext.handleDecrease()
})
const isDisabled = computed(() => rootContext.disabled?.value || props.disabled || rootContext.isMin.value)
const isDisabled = computed(() => rootContext.disabled?.value || props.disabled || rootContext.isDecreaseDisabled.value)
</script>

<template>
Expand All @@ -43,8 +43,7 @@ const isDisabled = computed(() => rootContext.disabled?.value || props.disabled
:disabled="isDisabled ? '' : undefined"
:data-disabled="isDisabled ? '' : undefined"
:data-pressed="isPressed ? 'true' : undefined"
@pointerdown.left="() => {
rootContext.inputEl.value?.focus()
@pointerdown.left.prevent="() => {
handlePressStart()
}"
@pointerup.left="() => {
Expand Down
5 changes: 2 additions & 3 deletions packages/radix-vue/src/NumberField/NumberFieldIncrement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ onTrigger(() => {
rootContext.handleIncrease()
})
const isDisabled = computed(() => rootContext.disabled?.value || props.disabled || rootContext.isMax.value)
const isDisabled = computed(() => rootContext.disabled?.value || props.disabled || rootContext.isIncreaseDisabled.value)
</script>

<template>
Expand All @@ -43,8 +43,7 @@ const isDisabled = computed(() => rootContext.disabled?.value || props.disabled
:disabled="isDisabled ? '' : undefined"
:data-disabled="isDisabled ? '' : undefined"
:data-pressed="isPressed ? 'true' : undefined"
@pointerdown.left="() => {
rootContext.inputEl.value?.focus()
@pointerdown.left.prevent="() => {
handlePressStart()
}"
@pointerup.left="() => {
Expand Down
1 change: 1 addition & 0 deletions packages/radix-vue/src/NumberField/NumberFieldInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function handleWheelEvent(event: WheelEvent) {
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX))
return
event.preventDefault()
if (event.deltaY > 0)
rootContext.handleIncrease()
else if (event.deltaY < 0)
Expand Down
51 changes: 34 additions & 17 deletions packages/radix-vue/src/NumberField/NumberFieldRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,28 @@ interface NumberFieldRootContext {
labelId: string
max: Ref<number | undefined>
min: Ref<number | undefined>
isMin: Ref<boolean>
isMax: Ref<boolean>
isDecreaseDisabled: Ref<boolean>
isIncreaseDisabled: Ref<boolean>
}
export const [injectNumberFieldRootContext, provideNumberFieldRootContext] = createContext<NumberFieldRootContext>('NumberFieldRoot')
</script>

<script setup lang="ts">
import { Primitive, usePrimitiveElement } from '@/Primitive'
import { useNumberFormatter, useNumberParser } from './utils'
import { handleDecimalOperation, useNumberFormatter, useNumberParser } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NumberFieldRootProps>(), {
as: 'div',
defaultValue: 0,
defaultValue: undefined,
step: 1,
})
const emits = defineEmits<NumberFieldRootEmits>()
const { disabled, min, max } = toRefs(props)
const { disabled, min, max, step } = toRefs(props)
const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
Expand All @@ -78,14 +79,32 @@ const labelId = useId('number-field-label')
const isFormControl = useFormControl(currentElement)
const inputEl = ref<HTMLInputElement>()
const isMin = computed(() => clampInputValue(modelValue.value) === min.value)
const isMax = computed(() => clampInputValue(modelValue.value) === max.value)
const isDecreaseDisabled = computed(() => (
clampInputValue(modelValue.value) === min.value
|| (min.value && !isNaN(modelValue.value) ? (handleDecimalOperation('-', modelValue.value, step.value) < min.value) : false)),
)
const isIncreaseDisabled = computed(() => (
clampInputValue(modelValue.value) === max.value
|| (max.value && !isNaN(modelValue.value) ? (handleDecimalOperation('+', modelValue.value, step.value) > max.value) : false)),
)
function handleChangingValue(type: 'increase' | 'decrease', multiplier = 1) {
if (isNaN(modelValue.value)) {
modelValue.value = min.value ?? 0
}
else {
if (type === 'increase')
modelValue.value = clampInputValue(modelValue.value + ((step.value ?? 1) * multiplier))
else
modelValue.value = clampInputValue(modelValue.value - ((step.value ?? 1) * multiplier))
}
}
function handleIncrease(multiplier = 1) {
modelValue.value = clampInputValue(modelValue.value + ((props.step ?? 1) * multiplier))
handleChangingValue('increase', multiplier)
}
function handleDecrease(multiplier = 1) {
modelValue.value = clampInputValue(modelValue.value - ((props.step ?? 1) * multiplier))
handleChangingValue('decrease', multiplier)
}
function handleMinMaxValue(type: 'min' | 'max') {
Expand All @@ -111,7 +130,7 @@ const inputMode = computed<HTMLAttributes['inputmode']>(() => {
// Replace negative textValue formatted using currencySign: 'accounting'
// with a textValue that can be announced using a minus sign.
const textValueFormatter = useNumberFormatter(props.locale, props.formatOptions)
const textValue = computed(() => Number.isNaN(modelValue.value) ? '' : textValueFormatter.format(modelValue.value))
const textValue = computed(() => isNaN(modelValue.value) ? '' : textValueFormatter.format(modelValue.value))
function validate(val: string) {
return numberParser.isValidPartialNumber(val, min.value, max.value)
Expand All @@ -125,10 +144,10 @@ function setInputValue(val: string) {
function clampInputValue(val: number) {
// Clamp to min and max, round to the nearest step, and round to specified number of digits
let clampedValue: number
if (props.step === undefined || Number.isNaN(props.step))
if (step.value === undefined || isNaN(step.value))
clampedValue = clamp(val, min.value, max.value)
else
clampedValue = snapValueToStep(val, min.value, max.value, props.step)
clampedValue = snapValueToStep(val, min.value, max.value, step.value)
clampedValue = numberParser.parse(numberFormatter.format(clampedValue))
return clampedValue
Expand All @@ -139,14 +158,12 @@ function applyInputValue(val: string) {
return
const parsedValue = numberParser.parse(val)
// modelValue.value = Number.isNaN(newValue) ? Number.NaN : newValue
// Set to empty state if input value is empty
if (!val.length)
return setInputValue(modelValue.value === undefined ? '' : textValue.value)
// if it failed to parse, then reset input to formatted version of current number
if (Number.isNaN(parsedValue))
if (isNaN(parsedValue))
return setInputValue(textValue.value)
modelValue.value = clampInputValue(parsedValue)
Expand All @@ -168,8 +185,8 @@ provideNumberFieldRootContext({
disabled,
max,
min,
isMin,
isMax,
isDecreaseDisabled,
isIncreaseDisabled,
})
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ import { Icon } from '@iconify/vue'
<Story title="NumberField/Basic" :layout="{ type: 'single', iframe: true }">
<Variant title="default">
<NumberFieldRoot
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white" :format-options="{
style: 'currency',
currency: 'EUR',
currencyDisplay: 'code',
currencySign: 'accounting',
}"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
:min="0"
:max="100"
:default-value="5"
>
<NumberFieldDecrement class="p-2">
<Icon icon="radix-icons:minus" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<script setup lang="ts">
import { ref } from 'vue'
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot } from '..'
import { Icon } from '@iconify/vue'
const value = ref(5)
</script>

<template>
<Story title="NumberField/Chromatic" :layout="{ type: 'grid', width: '50%' }">
<Variant title="Uncontrolled">
<NumberFieldRoot
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
:default-value="5"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Controlled">
<NumberFieldRoot
v-model="value"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Decimal">
<NumberFieldRoot
:default-value="0"
:format-options="{
signDisplay: 'exceptZero',
minimumFractionDigits: 1,
maximumFractionDigits: 2,
}"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Percentage">
<NumberFieldRoot
:default-value="0.05"
:step="0.01"
:format-options="{
style: 'percent',
}"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Currency values">
<NumberFieldRoot
:default-value="5"
:format-options="{
style: 'currency',
currency: 'EUR',
currencyDisplay: 'code',
currencySign: 'accounting',
}"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Units">
<NumberFieldRoot
:default-value="5"
:format-options="{
style: 'unit',
unit: 'inch',
unitDisplay: 'long',
}"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Minimum">
<NumberFieldRoot
:default-value="5"
:min="0"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Maximum">
<NumberFieldRoot
:default-value="5"
:max="20"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Step (3)">
<NumberFieldRoot
:step="3"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Step (3) + Minimum (2)">
<NumberFieldRoot
:min="2"
:step="3"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>

<Variant title="Step (3) + Minimum (2) + Maximum (21)">
<NumberFieldRoot
:min="2"
:max="21"
:step="3"
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
>
<NumberFieldDecrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:minus" />
</NumberFieldDecrement>
<NumberFieldInput class="bg-transparent w-20 tabular-nums focus:outline-0 p-1" />
<NumberFieldIncrement class="p-2 disabled:opacity-20">
<Icon icon="radix-icons:plus" />
</NumberFieldIncrement>
</NumberFieldRoot>
</Variant>
</Story>
</template>
Loading

0 comments on commit 2372d82

Please sign in to comment.