@@ -8,7 +8,17 @@ import { useFieldRootContext } from '../../field/root/FieldRootContext';
88import { useFieldControlValidation } from '../../field/control/useFieldControlValidation' ;
99import { fieldValidityMapping } from '../../field/utils/constants' ;
1010import { DEFAULT_STEP } from '../utils/constants' ;
11- import { ARABIC_RE , HAN_RE , getNumberLocaleDetails , parseNumber } from '../utils/parse' ;
11+ import {
12+ ARABIC_DETECT_RE ,
13+ HAN_DETECT_RE ,
14+ FULLWIDTH_DETECT_RE ,
15+ getNumberLocaleDetails ,
16+ parseNumber ,
17+ ANY_MINUS_RE ,
18+ ANY_PLUS_RE ,
19+ ANY_MINUS_DETECT_RE ,
20+ ANY_PLUS_DETECT_RE ,
21+ } from '../utils/parse' ;
1222import type { NumberFieldRoot } from '../root/NumberFieldRoot' ;
1323import { stateAttributesMapping as numberFieldStateAttributesMapping } from '../utils/stateAttributesMapping' ;
1424import { useField } from '../../field/useField' ;
@@ -171,43 +181,38 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
171181
172182 const formatOptions = formatOptionsRef . current ;
173183 const parsedValue = parseNumber ( inputValue , locale , formatOptions ) ;
174- const canonicalText = formatNumber ( parsedValue , locale , formatOptions ) ;
175- const maxPrecisionText = formatNumberMaxPrecision ( parsedValue , locale , formatOptions ) ;
176- const canonical = parseNumber ( canonicalText , locale , formatOptions ) ;
177- const maxPrecision = parseNumber ( maxPrecisionText , locale , formatOptions ) ;
178-
179184 if ( parsedValue === null ) {
180185 return ;
181186 }
182187
183188 blockRevalidationRef . current = true ;
184189
185- if ( validationMode === 'onBlur' ) {
186- commitValidation ( canonical ) ;
187- }
188-
190+ // If an explicit precision is requested, round the committed numeric value.
189191 const hasExplicitPrecision =
190192 formatOptions ?. maximumFractionDigits != null ||
191193 formatOptions ?. minimumFractionDigits != null ;
192194
193- if ( hasExplicitPrecision ) {
194- // When the consumer explicitly requests a precision, always round the number to that
195- // precision and normalize the displayed text accordingly.
196- if ( value !== canonical ) {
197- setValue ( canonical , event . nativeEvent ) ;
198- }
199- if ( inputValue !== canonicalText ) {
200- setInputValue ( canonicalText ) ;
201- }
202- } else if ( value !== maxPrecision ) {
203- // Default behaviour: preserve max precision until it differs from canonical
204- setValue ( canonical , event . nativeEvent ) ;
205- } else {
206- const shouldPreserveFullPrecision =
207- parsedValue === value && inputValue === maxPrecisionText ;
208- if ( ! shouldPreserveFullPrecision && inputValue !== canonicalText ) {
209- setInputValue ( canonicalText ) ;
210- }
195+ const maxFrac = formatOptions ?. maximumFractionDigits ;
196+ const committed =
197+ hasExplicitPrecision && typeof maxFrac === 'number'
198+ ? Number ( parsedValue . toFixed ( maxFrac ) )
199+ : parsedValue ;
200+
201+ if ( validationMode === 'onBlur' ) {
202+ commitValidation ( committed ) ;
203+ }
204+ if ( value !== committed ) {
205+ setValue ( committed , event . nativeEvent ) ;
206+ }
207+
208+ // Normalize only the displayed text
209+ const canonicalText = formatNumber ( committed , locale , formatOptions ) ;
210+ const maxPrecisionText = formatNumberMaxPrecision ( parsedValue , locale , formatOptions ) ;
211+ const shouldPreserveFullPrecision =
212+ ! hasExplicitPrecision && parsedValue === value && inputValue === maxPrecisionText ;
213+
214+ if ( ! shouldPreserveFullPrecision && inputValue !== canonicalText ) {
215+ setInputValue ( canonicalText ) ;
211216 }
212217 } ,
213218 onChange ( event ) {
@@ -251,20 +256,42 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
251256 let isAllowedNonNumericKey = allowedNonNumericKeys . has ( event . key ) ;
252257
253258 const { decimal, currency, percentSign } = getNumberLocaleDetails (
254- [ ] ,
259+ locale ,
255260 formatOptionsRef . current ,
256261 ) ;
257262
258263 const selectionStart = event . currentTarget . selectionStart ;
259264 const selectionEnd = event . currentTarget . selectionEnd ;
260265 const isAllSelected = selectionStart === 0 && selectionEnd === inputValue . length ;
261266
262- // Allow the minus key only if there isn't already a plus or minus sign, or if all the text
263- // is selected, or if only the minus sign is highlighted.
264- if ( event . key === '-' && allowedNonNumericKeys . has ( '-' ) ) {
265- const isMinusHighlighted =
266- selectionStart === 0 && selectionEnd === 1 && inputValue [ 0 ] === '-' ;
267- isAllowedNonNumericKey = ! inputValue . includes ( '-' ) || isAllSelected || isMinusHighlighted ;
267+ // Normalize handling of plus/minus signs via precomputed regexes
268+ const selectionIsExactlyCharAt = ( index : number ) =>
269+ selectionStart === index && selectionEnd === index + 1 ;
270+
271+ if (
272+ ANY_MINUS_DETECT_RE . test ( event . key ) &&
273+ Array . from ( allowedNonNumericKeys ) . some ( ( k ) => ANY_MINUS_DETECT_RE . test ( k || '' ) )
274+ ) {
275+ // Only allow one sign unless replacing the existing one or all text is selected
276+ const existingIndex = inputValue . search ( ANY_MINUS_RE ) ;
277+ const isReplacingExisting =
278+ existingIndex != null && existingIndex !== - 1 && selectionIsExactlyCharAt ( existingIndex ) ;
279+ isAllowedNonNumericKey =
280+ ! ( ANY_MINUS_DETECT_RE . test ( inputValue ) || ANY_PLUS_DETECT_RE . test ( inputValue ) ) ||
281+ isAllSelected ||
282+ isReplacingExisting ;
283+ }
284+ if (
285+ ANY_PLUS_DETECT_RE . test ( event . key ) &&
286+ Array . from ( allowedNonNumericKeys ) . some ( ( k ) => ANY_PLUS_DETECT_RE . test ( k || '' ) )
287+ ) {
288+ const existingIndex = inputValue . search ( ANY_PLUS_RE ) ;
289+ const isReplacingExisting =
290+ existingIndex != null && existingIndex !== - 1 && selectionIsExactlyCharAt ( existingIndex ) ;
291+ isAllowedNonNumericKey =
292+ ! ( ANY_MINUS_DETECT_RE . test ( inputValue ) || ANY_PLUS_DETECT_RE . test ( inputValue ) ) ||
293+ isAllSelected ||
294+ isReplacingExisting ;
268295 }
269296
270297 // Only allow one of each symbol.
@@ -279,8 +306,9 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
279306 } ) ;
280307
281308 const isLatinNumeral = / ^ [ 0 - 9 ] $ / . test ( event . key ) ;
282- const isArabicNumeral = ARABIC_RE . test ( event . key ) ;
283- const isHanNumeral = HAN_RE . test ( event . key ) ;
309+ const isArabicNumeral = ARABIC_DETECT_RE . test ( event . key ) ;
310+ const isHanNumeral = HAN_DETECT_RE . test ( event . key ) ;
311+ const isFullwidthNumeral = FULLWIDTH_DETECT_RE . test ( event . key ) ;
284312 const isNavigateKey = NAVIGATE_KEYS . has ( event . key ) ;
285313
286314 if (
@@ -294,6 +322,7 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
294322 isAllowedNonNumericKey ||
295323 isLatinNumeral ||
296324 isArabicNumeral ||
325+ isFullwidthNumeral ||
297326 isHanNumeral ||
298327 isNavigateKey
299328 ) {
0 commit comments