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
+ });
});