diff --git a/packages/@internationalized/number/src/NumberParser.ts b/packages/@internationalized/number/src/NumberParser.ts index ae6448f027d..10ad0d60195 100644 --- a/packages/@internationalized/number/src/NumberParser.ts +++ b/packages/@internationalized/number/src/NumberParser.ts @@ -19,7 +19,9 @@ interface Symbols { group?: string, literals: RegExp, numeral: RegExp, - index: (v: string) => string + numerals: string[], + index: (v: string) => string, + noNumeralUnits: Array<{unit: string, value: number}> } const CURRENCY_SIGN_REGEX = new RegExp('^.*\\(.*\\).*$'); @@ -130,13 +132,17 @@ class NumberParserImpl { } parse(value: string) { + let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; // to parse the number, we need to remove anything that isn't actually part of the number, for example we want '-10.40' not '-10.40 USD' let fullySanitizedValue = this.sanitize(value); - if (this.symbols.group) { - // Remove group characters, and replace decimal points and numerals with ASCII values. - fullySanitizedValue = replaceAll(fullySanitizedValue, this.symbols.group, ''); + // Return NaN if there is a group symbol but useGrouping is false + if (!isGroupSymbolAllowed && this.symbols.group && fullySanitizedValue.includes(this.symbols.group)) { + return NaN; + } else if (this.symbols.group) { + fullySanitizedValue = fullySanitizedValue.replaceAll(this.symbols.group!, ''); } + if (this.symbols.decimal) { fullySanitizedValue = fullySanitizedValue.replace(this.symbols.decimal!, '.'); } @@ -189,13 +195,34 @@ class NumberParserImpl { if (this.options.currencySign === 'accounting' && CURRENCY_SIGN_REGEX.test(value)) { newValue = -1 * newValue; } - return newValue; } sanitize(value: string) { - // Remove literals and whitespace, which are allowed anywhere in the string - value = value.replace(this.symbols.literals, ''); + let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; + // If the value is only a unit and it matches one of the formatted numbers where the value is part of the unit and doesn't have any numerals, then + // return the known value for that case. + if (this.symbols.noNumeralUnits.length > 0 && this.symbols.noNumeralUnits.find(obj => obj.unit === value)) { + return this.symbols.noNumeralUnits.find(obj => obj.unit === value)!.value.toString(); + } + // Do our best to preserve the number and its possible group and decimal symbols, this includes the sign as well + let preservedInsideNumber = value.match(new RegExp(`([${this.symbols.numerals.join('')}].*[${this.symbols.numerals.join('')}])`)); + if (preservedInsideNumber) { + // If we found a number, replace literals everywhere except inside the number + let beforeNumber = value.substring(0, preservedInsideNumber.index!); + let afterNumber = value.substring(preservedInsideNumber.index! + preservedInsideNumber[0].length); + let insideNumber = preservedInsideNumber[0]; + + // Replace literals in the parts outside the number + beforeNumber = beforeNumber.replace(this.symbols.literals, ''); + afterNumber = afterNumber.replace(this.symbols.literals, ''); + + // Reconstruct the value with literals removed from outside the number + value = beforeNumber + insideNumber + afterNumber; + } else { + // If no number found, replace literals everywhere + value = value.replace(this.symbols.literals, ''); + } // Replace the ASCII minus sign with the minus sign used in the current locale // so that both are allowed in case the user's keyboard doesn't have the locale's minus sign. @@ -207,31 +234,94 @@ class NumberParserImpl { // instead they use the , (44) character or apparently the (1548) character. if (this.options.numberingSystem === 'arab') { if (this.symbols.decimal) { - value = value.replace(',', this.symbols.decimal); - value = value.replace(String.fromCharCode(1548), this.symbols.decimal); + value = replaceAll(value, ',', this.symbols.decimal); + value = replaceAll(value, String.fromCharCode(1548), this.symbols.decimal); } - if (this.symbols.group) { + if (this.symbols.group && isGroupSymbolAllowed) { value = replaceAll(value, '.', this.symbols.group); } } // In some locale styles, such as swiss currency, the group character can be a special single quote // that keyboards don't typically have. This expands the character to include the easier to type single quote. - if (this.symbols.group === '’' && value.includes("'")) { + if (this.symbols.group === '’' && value.includes("'") && isGroupSymbolAllowed) { value = replaceAll(value, "'", this.symbols.group); } // fr-FR group character is narrow non-breaking space, char code 8239 (U+202F), but that's not a key on the french keyboard, // so allow space and non-breaking space as a group char as well - if (this.options.locale === 'fr-FR' && this.symbols.group) { + if (this.options.locale === 'fr-FR' && this.symbols.group && isGroupSymbolAllowed) { value = replaceAll(value, ' ', this.symbols.group); value = replaceAll(value, /\u00A0/g, this.symbols.group); } + // If there are multiple decimal separators and only one group separator, swap them + if (this.symbols.decimal + && (this.symbols.group && isGroupSymbolAllowed) + && [...value.matchAll(new RegExp(escapeRegex(this.symbols.decimal), 'g'))].length > 1 + && [...value.matchAll(new RegExp(escapeRegex(this.symbols.group), 'g'))].length <= 1) { + value = swapCharacters(value, this.symbols.decimal, this.symbols.group); + } + + // If the decimal separator is before the group separator, swap them + let decimalIndex = value.indexOf(this.symbols.decimal!); + let groupIndex = value.indexOf(this.symbols.group!); + if (this.symbols.decimal && (this.symbols.group && isGroupSymbolAllowed) && decimalIndex > -1 && groupIndex > -1 && decimalIndex < groupIndex) { + value = swapCharacters(value, this.symbols.decimal, this.symbols.group); + } + + // in the value, for any non-digits and not the plus/minus sign, + // if there is only one of that character and its index in the string is 0 or it's only preceeded by this numbering system's "0" character, + // then we could try to guess that it's a decimal character and replace it, but it's too ambiguous, a user may be deleting 1,024 -> ,024 and + // we don't want to change 24 into .024 + let temp = value; + if (this.symbols.minusSign) { + temp = replaceAll(temp, this.symbols.minusSign, ''); + temp = replaceAll(temp, '\u2212', ''); + } + if (this.symbols.plusSign) { + temp = replaceAll(temp, this.symbols.plusSign, ''); + } + temp = replaceAll(temp, new RegExp(`^${escapeRegex(this.symbols.numerals[0])}+`, 'g'), ''); + let nonDigits = new Set(replaceAll(temp, this.symbols.numeral, '').split('')); + + // This is to fuzzy match group and decimal symbols from a different formatting, we can only do it if there are 2 non-digits, otherwise it's too ambiguous + let areOnlyGroupAndDecimalSymbols = [...nonDigits].every(char => allPossibleGroupAndDecimalSymbols.has(char)); + let oneSymbolNotMatching = ( + nonDigits.size === 2 + && (this.symbols.group && isGroupSymbolAllowed) + && this.symbols.decimal + && (!nonDigits.has(this.symbols.group!) || !nonDigits.has(this.symbols.decimal!)) + ); + let bothSymbolsNotMatching = ( + nonDigits.size === 2 + && (this.symbols.group && isGroupSymbolAllowed) + && this.symbols.decimal + && !nonDigits.has(this.symbols.group!) && !nonDigits.has(this.symbols.decimal!) + ); + if (areOnlyGroupAndDecimalSymbols && (oneSymbolNotMatching || bothSymbolsNotMatching)) { + // Try to determine which of the nonDigits is the group and which is the decimal + // Whichever of the nonDigits is first in the string is the group. + // If there are more than one of a nonDigit, then that one is the group. + let [firstChar, secondChar] = [...nonDigits]; + if (value.indexOf(firstChar) < value.indexOf(secondChar)) { + value = replaceAll(value, firstChar, '__GROUP__'); + value = replaceAll(value, secondChar, '__DECIMAL__'); + value = replaceAll(value, '__GROUP__', this.symbols.group!); + value = replaceAll(value, '__DECIMAL__', this.symbols.decimal!); + } else { + value = replaceAll(value, secondChar, '__GROUP__'); + value = replaceAll(value, firstChar, '__DECIMAL__'); + value = replaceAll(value, '__GROUP__', this.symbols.group!); + value = replaceAll(value, '__DECIMAL__', this.symbols.decimal!); + } + } + return value; } isValidPartialNumber(value: string, minValue: number = -Infinity, maxValue: number = Infinity): boolean { + let isGroupSymbolAllowed = this.formatter.resolvedOptions().useGrouping; value = this.sanitize(value); // Remove minus or plus sign, which must be at the start of the string. @@ -241,18 +331,13 @@ class NumberParserImpl { value = value.slice(this.symbols.plusSign.length); } - // Numbers cannot start with a group separator - if (this.symbols.group && value.startsWith(this.symbols.group)) { - return false; - } - // Numbers that can't have any decimal values fail if a decimal character is typed if (this.symbols.decimal && value.indexOf(this.symbols.decimal) > -1 && this.options.maximumFractionDigits === 0) { return false; } // Remove numerals, groups, and decimals - if (this.symbols.group) { + if (this.symbols.group && isGroupSymbolAllowed) { value = replaceAll(value, this.symbols.group, ''); } value = value.replace(this.symbols.numeral, ''); @@ -267,6 +352,9 @@ class NumberParserImpl { const nonLiteralParts = new Set(['decimal', 'fraction', 'integer', 'minusSign', 'plusSign', 'group']); +// This list is a best guess at the moment +const allPossibleGroupAndDecimalSymbols = new Set(['.', ',', ' ', String.fromCharCode(1548), '\u00A0', "'"]); + // This list is derived from https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html#comparison and includes // all unique numbers which we need to check in order to determine all the plural forms for a given locale. // See: https://github.com/adobe/react-spectrum/pull/5134/files#r1337037855 for used script @@ -282,12 +370,21 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I maximumSignificantDigits: 21, roundingIncrement: 1, roundingPriority: 'auto', - roundingMode: 'halfExpand' + roundingMode: 'halfExpand', + useGrouping: true }); // Note: some locale's don't add a group symbol until there is a ten thousands place let allParts = symbolFormatter.formatToParts(-10000.111); let posAllParts = symbolFormatter.formatToParts(10000.111); let pluralParts = pluralNumbers.map(n => symbolFormatter.formatToParts(n)); + // if the plural parts include a unit but no integer or fraction, then we need to add the unit to the special set + let noNumeralUnits = pluralParts.map((p, i) => { + let unit = p.find(p => p.type === 'unit'); + if (unit && !p.some(p => p.type === 'integer' || p.type === 'fraction')) { + return {unit: unit.value, value: pluralNumbers[i]}; + } + return null; + }).filter(p => !!p); let minusSign = allParts.find(p => p.type === 'minusSign')?.value ?? '-'; let plusSign = posAllParts.find(p => p.type === 'plusSign')?.value; @@ -311,9 +408,10 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I let pluralPartsLiterals = pluralParts.flatMap(p => p.filter(p => !nonLiteralParts.has(p.type)).map(p => escapeRegex(p.value))); let sortedLiterals = [...new Set([...allPartsLiterals, ...pluralPartsLiterals])].sort((a, b) => b.length - a.length); + // Match both whitespace and formatting characters let literals = sortedLiterals.length === 0 ? - new RegExp('[\\p{White_Space}]', 'gu') : - new RegExp(`${sortedLiterals.join('|')}|[\\p{White_Space}]`, 'gu'); + new RegExp('\\p{White_Space}|\\p{Cf}', 'gu') : + new RegExp(`${sortedLiterals.join('|')}|\\p{White_Space}|\\p{Cf}`, 'gu'); // These are for replacing non-latn characters with the latn equivalent let numerals = [...new Intl.NumberFormat(intlOptions.locale, {useGrouping: false}).format(9876543210)].reverse(); @@ -321,7 +419,15 @@ function getSymbols(locale: string, formatter: Intl.NumberFormat, intlOptions: I let numeral = new RegExp(`[${numerals.join('')}]`, 'g'); let index = d => String(indexes.get(d)); - return {minusSign, plusSign, decimal, group, literals, numeral, index}; + return {minusSign, plusSign, decimal, group, literals, numeral, numerals, index, noNumeralUnits}; +} + +function swapCharacters(str: string, char1: string, char2: string) { + const tempChar = '_TEMP_'; + let result = str.replaceAll(char1, tempChar); + result = result.replaceAll(char2, char1); + result = result.replaceAll(tempChar, char2); + return result; } function replaceAll(str: string, find: string | RegExp, replace: string) { diff --git a/packages/@internationalized/number/test/NumberParser.test.js b/packages/@internationalized/number/test/NumberParser.test.js index a9266d997cf..bc14876ef6c 100644 --- a/packages/@internationalized/number/test/NumberParser.test.js +++ b/packages/@internationalized/number/test/NumberParser.test.js @@ -38,6 +38,11 @@ describe('NumberParser', function () { expect(new NumberParser('en-US', {style: 'decimal'}).parse('-1,000,000')).toBe(-1000000); }); + it('should support accidentally using a group character as a decimal character', function () { + expect(new NumberParser('en-US', {style: 'decimal'}).parse('1.000,00')).toBe(1000); + expect(new NumberParser('en-US', {style: 'decimal'}).parse('1.000.000,00')).toBe(1000000); + }); + it('should support signDisplay', function () { expect(new NumberParser('en-US', {style: 'decimal'}).parse('+10')).toBe(10); expect(new NumberParser('en-US', {style: 'decimal', signDisplay: 'always'}).parse('+10')).toBe(10); @@ -56,6 +61,11 @@ describe('NumberParser', function () { expect(new NumberParser('en-US', {style: 'decimal'}).parse('1abc')).toBe(NaN); }); + it('should return NaN for invalid grouping', function () { + expect(new NumberParser('en-US', {useGrouping: false}).parse('1234,7')).toBeNaN(); + expect(new NumberParser('de-DE', {useGrouping: false}).parse('1234.7')).toBeNaN(); + }); + describe('currency', function () { it('should parse without the currency symbol', function () { expect(new NumberParser('en-US', {currency: 'USD', style: 'currency'}).parse('10.50')).toBe(10.5); @@ -194,8 +204,13 @@ describe('NumberParser', function () { expect(new NumberParser('de-CH', {style: 'currency', currency: 'CHF'}).parse("CHF 1'000.00")).toBe(1000); }); + it('should parse arabic singular and dual counts', () => { + expect(new NumberParser('ar-AE', {style: 'unit', unit: 'day', unitDisplay: 'long'}).parse('يومان')).toBe(2); + expect(new NumberParser('ar-AE', {style: 'unit', unit: 'day', unitDisplay: 'long'}).parse('يوم')).toBe(1); + }); + describe('round trips', function () { - fc.configureGlobal({numRuns: 200}); + fc.configureGlobal({numRuns: 2000}); // Locales have to include: 'de-DE', 'ar-EG', 'fr-FR' and possibly others // But for the moment they are not properly supported const localesArb = fc.constantFrom(...locales); @@ -301,6 +316,78 @@ describe('NumberParser', function () { const formattedOnce = formatter.format(1); expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); }); + it('should handle small numbers', () => { + let locale = 'ar-AE'; + let options = { + style: 'decimal', + minimumIntegerDigits: 4, + maximumSignificantDigits: 1 + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(2.220446049250313e-16); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); + it('should handle currency small numbers', () => { + let locale = 'ar-AE-u-nu-latn'; + let options = { + style: 'currency', + currency: 'USD' + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(2.220446049250313e-16); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); + it('should handle hanidec small numbers', () => { + let locale = 'ar-AE-u-nu-hanidec'; + let options = { + style: 'decimal' + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(2.220446049250313e-16); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); + it('should handle beng with minimum integer digits', () => { + let locale = 'ar-AE-u-nu-beng'; + let options = { + style: 'decimal', + minimumIntegerDigits: 4, + maximumFractionDigits: 0 + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(2.220446049250313e-16); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); + it('should handle percent with minimum integer digits', () => { + let locale = 'ar-AE-u-nu-latn'; + let options = { + style: 'percent', + minimumIntegerDigits: 4, + minimumFractionDigits: 9, + maximumSignificantDigits: 1, + maximumFractionDigits: undefined + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(0.0095); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); + it('should handle non-grouping in russian locale', () => { + let locale = 'ru-RU'; + let options = { + style: 'percent', + useGrouping: false, + minimumFractionDigits: undefined, + maximumFractionDigits: undefined + }; + const formatter = new Intl.NumberFormat(locale, options); + const parser = new NumberParser(locale, options); + const formattedOnce = formatter.format(2.220446049250313e-16); + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); + }); }); }); @@ -327,14 +414,21 @@ describe('NumberParser', function () { }); it('should support group characters', function () { - expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber(',')).toBe(true); // en-US-u-nu-arab uses commas as the decimal point character - expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber(',000')).toBe(false); // latin numerals cannot follow arab decimal point + // starting with arabic decimal point + expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber(',')).toBe(true); + expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber(',000')).toBe(true); + expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('000,000')).toBe(true); expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('1,000')).toBe(true); expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('-1,000')).toBe(true); expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('1,000,000')).toBe(true); expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('-1,000,000')).toBe(true); }); + it('should return false for invalid grouping', function () { + expect(new NumberParser('en-US', {useGrouping: false}).isValidPartialNumber('1234,7')).toBe(false); + expect(new NumberParser('de-DE', {useGrouping: false}).isValidPartialNumber('1234.7')).toBe(false); + }); + it('should reject random characters', function () { expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('g')).toBe(false); expect(new NumberParser('en-US', {style: 'decimal'}).isValidPartialNumber('1abc')).toBe(false); diff --git a/packages/@react-spectrum/numberfield/test/NumberField.test.js b/packages/@react-spectrum/numberfield/test/NumberField.test.js index c5574309aec..ef74148babe 100644 --- a/packages/@react-spectrum/numberfield/test/NumberField.test.js +++ b/packages/@react-spectrum/numberfield/test/NumberField.test.js @@ -2042,6 +2042,19 @@ describe('NumberField', function () { expect(textField).toHaveAttribute('value', formatter.format(21)); }); + it('should maintain original parser and formatting when restoring a previous value', async () => { + let {textField} = renderNumberField({onChange: onChangeSpy, defaultValue: 10}); + expect(textField).toHaveAttribute('value', '10'); + + await user.tab(); + await user.clear(textField); + await user.keyboard(',123'); + act(() => {textField.blur();}); + expect(textField).toHaveAttribute('value', '123'); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenCalledWith(123); + }); + describe('beforeinput', () => { let getTargetRanges = InputEvent.prototype.getTargetRanges; beforeEach(() => { diff --git a/packages/react-aria-components/stories/NumberField.stories.tsx b/packages/react-aria-components/stories/NumberField.stories.tsx index 93d0a94ce62..9f7cf521fc5 100644 --- a/packages/react-aria-components/stories/NumberField.stories.tsx +++ b/packages/react-aria-components/stories/NumberField.stories.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {Button, FieldError, Group, Input, Label, NumberField, NumberFieldProps} from 'react-aria-components'; +import {Button, FieldError, Group, I18nProvider, Input, Label, NumberField, NumberFieldProps} from 'react-aria-components'; import {Meta, StoryObj} from '@storybook/react'; import React, {useState} from 'react'; import './styles.css'; @@ -72,3 +72,23 @@ export const NumberFieldControlledExample = { ) }; + +export const ArabicNumberFieldExample = { + args: { + defaultValue: 0, + formatOptions: {style: 'unit', unit: 'day', unitDisplay: 'long'} + }, + render: (args) => ( + + (v & 1 ? 'Invalid value' : null)}> + + + + + + + + + + ) +}; diff --git a/packages/react-aria-components/test/NumberField.test.js b/packages/react-aria-components/test/NumberField.test.js index d29514fe54c..b0e7a5a1fc9 100644 --- a/packages/react-aria-components/test/NumberField.test.js +++ b/packages/react-aria-components/test/NumberField.test.js @@ -11,7 +11,7 @@ */ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; -import {Button, FieldError, Group, Input, Label, NumberField, NumberFieldContext, Text} from '../'; +import {Button, FieldError, Group, I18nProvider, Input, Label, NumberField, NumberFieldContext, Text} from '../'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -189,4 +189,114 @@ describe('NumberField', () => { expect(input).not.toHaveAttribute('aria-describedby'); expect(numberfield).not.toHaveAttribute('data-invalid'); }); + + it('supports pasting value in another numbering system', async () => { + let {getByRole, rerender} = render(); + let input = getByRole('textbox'); + await user.tab(); + act(() => { + input.setSelectionRange(0, input.value.length); + }); + await user.paste('3.000.000,25'); + await user.keyboard('{Enter}'); + expect(input).toHaveValue('3,000,000.25'); + + act(() => { + input.setSelectionRange(0, input.value.length); + }); + await user.paste('3 000 000,25'); + await user.keyboard('{Enter}'); + expect(input).toHaveValue('3,000,000.25'); + + rerender(); + + act(() => { + input.setSelectionRange(0, input.value.length); + }); + await user.paste('3 000 000,256789'); + await user.keyboard('{Enter}'); + expect(input).toHaveValue('$3,000,000.26'); + + act(() => { + input.setSelectionRange(0, input.value.length); + }); + await user.paste('1,000'); + await user.keyboard('{Enter}'); + expect(input).toHaveValue('$1,000.00', 'Ambiguous value should be parsed using the current locale'); + + act(() => { + input.setSelectionRange(0, input.value.length); + }); + + await user.paste('1.000'); + await user.keyboard('{Enter}'); + expect(input).toHaveValue('$1.00', 'Ambiguous value should be parsed using the current locale'); + }); + + it('should support arabic singular and dual counts', async () => { + let onChange = jest.fn(); + let {getByRole} = render( + + + + + + + + + + + + ); + let input = getByRole('textbox'); + await user.tab(); + await user.keyboard('{ArrowUp}'); + expect(onChange).toHaveBeenLastCalledWith(1); + expect(input).toHaveValue('يوم'); + + await user.keyboard('{ArrowUp}'); + expect(input).toHaveValue('يومان'); + expect(onChange).toHaveBeenLastCalledWith(2); + }); + + it('should not type the grouping characters when useGrouping is false', async () => { + let {getByRole} = render(); + let input = getByRole('textbox'); + + await user.keyboard('102,4'); + expect(input).toHaveAttribute('value', '1024'); + + await user.clear(input); + expect(input).toHaveAttribute('value', ''); + + await user.paste('102,4'); + await user.tab(); + expect(input).toHaveAttribute('value', ''); + + await user.paste('1,024'); + await user.tab(); + expect(input).toHaveAttribute('value', ''); + // TODO: both of the above should parse to 1024 + + }); + + it('should not type the grouping characters when useGrouping is false and in German locale', async () => { + let {getByRole} = render(); + let input = getByRole('textbox'); + + await user.keyboard('102.4'); + expect(input).toHaveAttribute('value', '1024'); + + await user.clear(input); + expect(input).toHaveAttribute('value', ''); + + await user.paste('102.4'); + await user.tab(); + expect(input).toHaveAttribute('value', ''); + + await user.paste('1.024'); + await user.tab(); + expect(input).toHaveAttribute('value', ''); + // TODO: both of the above should parse to 1024 + }); });