From 6bb6002e1e1f54f72fe57172b78be2394bc38d38 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Tue, 30 Sep 2025 00:37:23 +0200 Subject: [PATCH] feat(VNumberInput): add `grouping` prop --- .../src/locale/en/VNumberInput.json | 2 + packages/docs/src/data/new-in.json | 4 +- .../components/VNumberInput/VNumberInput.tsx | 60 ++++++++++--------- packages/vuetify/src/composables/locale.ts | 2 + .../vuetify/src/locale/adapters/vuetify.ts | 7 +++ 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/api-generator/src/locale/en/VNumberInput.json b/packages/api-generator/src/locale/en/VNumberInput.json index 615bbc346fc..035bfc78de3 100644 --- a/packages/api-generator/src/locale/en/VNumberInput.json +++ b/packages/api-generator/src/locale/en/VNumberInput.json @@ -2,6 +2,8 @@ "props": { "controlVariant": "The color of the control. It defaults to the value of `variant` prop.", "decimalSeparator": "Expects single character to be used as decimal separator.", + "grouping ": "Enables grouping using current locale or specific character passed to `group-separator`.", + "group-separator ": "Expects single character to be used for grouping digits in large numbers.", "hideInput": "Hide the input field.", "inset": "Applies an indentation to the dividers used in the stepper buttons.", "max": "Specifies the maximum allowable value for the input.", diff --git a/packages/docs/src/data/new-in.json b/packages/docs/src/data/new-in.json index 556df2eeac9..87f87fa0fe1 100644 --- a/packages/docs/src/data/new-in.json +++ b/packages/docs/src/data/new-in.json @@ -178,7 +178,9 @@ }, "VNumberInput": { "props": { - "autocomplete": "3.10.0" + "autocomplete": "3.10.0", + "grouping": "3.11.0", + "groupingSeparator": "3.11.0" } }, "VOverlay": { diff --git a/packages/vuetify/src/components/VNumberInput/VNumberInput.tsx b/packages/vuetify/src/components/VNumberInput/VNumberInput.tsx index 2f607fd4e1b..bebe5acbd74 100644 --- a/packages/vuetify/src/components/VNumberInput/VNumberInput.tsx +++ b/packages/vuetify/src/components/VNumberInput/VNumberInput.tsx @@ -68,6 +68,14 @@ const makeVNumberInputProps = propsFactory({ type: String, validator: (v: any) => !v || v.length === 1, }, + grouping: { + type: [Boolean, String] as PropType<'always' | 'auto' | 'min2' | boolean>, + default: false, + }, + groupSeparator: { + type: String, + validator: (v: any) => !v || v.length === 1, + }, ...omit(makeVTextFieldProps(), ['modelValue', 'validationValue']), }, 'VNumberInput') @@ -95,32 +103,23 @@ export const VNumberInput = genericComponent()({ const isFocused = shallowRef(props.focused) - const { decimalSeparator: decimalSeparatorFromLocale } = useLocale() + const { + current: locale, + decimalSeparator: decimalSeparatorFromLocale, + numericGroupSeparator: numericGroupSeparatorFromLocale, + } = useLocale() const decimalSeparator = computed(() => props.decimalSeparator?.[0] || decimalSeparatorFromLocale.value) - - function correctPrecision (val: number, precision = props.precision, trim = true) { - const fixed = precision == null - ? String(val) - : val.toFixed(precision) - - if (isFocused.value && trim) { - return Number(fixed).toString() // trim zeros - .replace('.', decimalSeparator.value) - } - - if (props.minFractionDigits === null || (precision !== null && precision < props.minFractionDigits)) { - return fixed.replace('.', decimalSeparator.value) - } - - let [baseDigits, fractionDigits] = fixed.split('.') - - fractionDigits = (fractionDigits ?? '').padEnd(props.minFractionDigits, '0') - .replace(new RegExp(`(?<=\\d{${props.minFractionDigits}})0+$`, 'g'), '') - - return [ - baseDigits, - fractionDigits, - ].filter(Boolean).join(decimalSeparator.value) + const groupSeparator = computed(() => props.groupSeparator?.[0] || numericGroupSeparatorFromLocale.value) + + function correctPrecision (val: number, precision?: number | null, trim = true) { + precision ??= isFocused.value && trim ? undefined : props.precision ?? undefined + return new Intl.NumberFormat(locale.value, { + minimumFractionDigits: props.minFractionDigits ?? precision, + maximumFractionDigits: precision, + useGrouping: props.grouping, + }).format(val) + .replaceAll(numericGroupSeparatorFromLocale.value, groupSeparator.value) + .replace(decimalSeparatorFromLocale.value, decimalSeparator.value) } const model = useProxiedModel(props, 'modelValue', null, @@ -154,7 +153,10 @@ export const VNumberInput = genericComponent()({ _inputText.value = null return } - const parsedValue = Number(val.replace(decimalSeparator.value, '.')) + const parsedValue = Number(val + .replaceAll(groupSeparator.value, '') + .replace(decimalSeparator.value, '.') + ) if (!isNaN(parsedValue) && parsedValue <= props.max && parsedValue >= props.min) { model.value = parsedValue _inputText.value = val @@ -315,7 +317,11 @@ export const VNumberInput = genericComponent()({ if (controlsDisabled.value) return if (!vTextFieldRef.value) return const actualText = vTextFieldRef.value.value - const parsedValue = Number(actualText.replace(decimalSeparator.value, '.')) + const parsedValue = Number(actualText + .replaceAll(groupSeparator.value, '') + .replace(decimalSeparator.value, '.') + ) + console.log('parsed value 2:', actualText, groupSeparator.value, parsedValue) if (actualText && !isNaN(parsedValue)) { inputText.value = correctPrecision(clamp(parsedValue, props.min, props.max)) } else { diff --git a/packages/vuetify/src/composables/locale.ts b/packages/vuetify/src/composables/locale.ts index 975747f08c8..89f7f03582c 100644 --- a/packages/vuetify/src/composables/locale.ts +++ b/packages/vuetify/src/composables/locale.ts @@ -11,6 +11,7 @@ export interface LocaleMessages { export interface LocaleOptions { decimalSeparator?: string + numericGroupSeparator?: string messages?: LocaleMessages locale?: string fallback?: string @@ -20,6 +21,7 @@ export interface LocaleOptions { export interface LocaleInstance { name: string decimalSeparator: ShallowRef + numericGroupSeparator: ShallowRef messages: Ref current: Ref fallback: Ref diff --git a/packages/vuetify/src/locale/adapters/vuetify.ts b/packages/vuetify/src/locale/adapters/vuetify.ts index ab860444064..51f83266621 100644 --- a/packages/vuetify/src/locale/adapters/vuetify.ts +++ b/packages/vuetify/src/locale/adapters/vuetify.ts @@ -68,6 +68,11 @@ function inferDecimalSeparator (current: Ref, fallback: Ref) { return format(0.1).includes(',') ? ',' : '.' } +function inferNumericGroupSeparator (current: Ref, fallback: Ref) { + const format = createNumberFunction(current, fallback) + return format(10000).at(2)! +} + function useProvided (props: any, prop: string, provided: Ref) { const internal = useProxiedModel(props, prop, props[prop] ?? provided.value) @@ -95,6 +100,7 @@ function createProvideFunction (state: { current: Ref, fallback: Ref inferDecimalSeparator(current, fallback)), + numericGroupSeparator: toRef(() => inferNumericGroupSeparator(current, fallback)), t: createTranslateFunction(current, fallback, messages), n: createNumberFunction(current, fallback), provide: createProvideFunction({ current, fallback, messages }), @@ -113,6 +119,7 @@ export function createVuetifyAdapter (options?: LocaleOptions): LocaleInstance { fallback, messages, decimalSeparator: toRef(() => options?.decimalSeparator ?? inferDecimalSeparator(current, fallback)), + numericGroupSeparator: toRef(() => options?.numericGroupSeparator ?? inferNumericGroupSeparator(current, fallback)), t: createTranslateFunction(current, fallback, messages), n: createNumberFunction(current, fallback), provide: createProvideFunction({ current, fallback, messages }),