@@ -99,6 +99,17 @@ export function useNumberFieldRoot(
99
99
const allowInputSyncRef = React . useRef ( true ) ;
100
100
const unsubscribeFromGlobalContextMenuRef = React . useRef < ( ) => void > ( ( ) => { } ) ;
101
101
102
+ const isControlled = externalValue !== undefined ;
103
+ const lastExternalValueRef = React . useRef < number | null | undefined > ( externalValue ) ;
104
+ const externalUpdateRef = React . useRef ( false ) ;
105
+
106
+ useModernLayoutEffect ( ( ) => {
107
+ if ( isControlled && externalValue !== lastExternalValueRef . current ) {
108
+ externalUpdateRef . current = true ;
109
+ }
110
+ lastExternalValueRef . current = externalValue ;
111
+ } , [ externalValue , isControlled ] ) ;
112
+
102
113
useModernLayoutEffect ( ( ) => {
103
114
if ( validityData . initialValue === null && value !== validityData . initialValue ) {
104
115
setValidityData ( ( prev ) => ( { ...prev , initialValue : value } ) ) ;
@@ -157,6 +168,22 @@ export function useNumberFieldRoot(
157
168
setValueUnwrapped ( validatedValue ) ;
158
169
setDirty ( validatedValue !== validityData . initialValue ) ;
159
170
171
+ if ( dir != null ) {
172
+ const wasExternal = externalUpdateRef . current ;
173
+ externalUpdateRef . current = false ;
174
+
175
+ const text = wasExternal
176
+ ? formatNumber ( validatedValue , locale , {
177
+ ...formatOptionsRef . current ,
178
+ maximumFractionDigits : 20 ,
179
+ } )
180
+ : formatNumber ( validatedValue , locale , formatOptionsRef . current ) ;
181
+
182
+ allowInputSyncRef . current = false ;
183
+ setInputValue ( text ) ;
184
+ return ;
185
+ }
186
+
160
187
// We need to force a re-render, because while the value may be unchanged, the formatting may
161
188
// be different. This forces the `useModernLayoutEffect` to run which acts as a single source of
162
189
// truth to sync the input value.
@@ -246,10 +273,20 @@ export function useNumberFieldRoot(
246
273
return ;
247
274
}
248
275
249
- const nextInputValue = formatNumber ( value , locale , formatOptionsRef . current ) ;
250
-
251
- if ( nextInputValue !== inputValue ) {
252
- setInputValue ( nextInputValue ) ;
276
+ if ( isControlled && externalUpdateRef . current ) {
277
+ // Respect precision if externally changed
278
+ const fullPrecision = formatNumber ( value , locale , {
279
+ ...formatOptionsRef . current ,
280
+ maximumFractionDigits : 20 ,
281
+ } ) ;
282
+ if ( fullPrecision !== inputValue ) {
283
+ setInputValue ( fullPrecision ) ;
284
+ }
285
+ } else {
286
+ const next = formatNumber ( value , locale , formatOptionsRef . current ) ;
287
+ if ( next !== inputValue ) {
288
+ setInputValue ( next ) ;
289
+ }
253
290
}
254
291
} ) ;
255
292
@@ -345,6 +382,8 @@ export function useNumberFieldRoot(
345
382
locale,
346
383
isScrubbing,
347
384
setIsScrubbing,
385
+ externalUpdateRef,
386
+ isControlled,
348
387
} ) ,
349
388
[
350
389
inputRef ,
@@ -376,6 +415,7 @@ export function useNumberFieldRoot(
376
415
setInputValue ,
377
416
locale ,
378
417
isScrubbing ,
418
+ isControlled ,
379
419
] ,
380
420
) ;
381
421
}
@@ -512,5 +552,7 @@ export namespace useNumberFieldRoot {
512
552
locale : Intl . LocalesArgument ;
513
553
isScrubbing : boolean ;
514
554
setIsScrubbing : React . Dispatch < React . SetStateAction < boolean > > ;
555
+ externalUpdateRef : React . RefObject < boolean | null > ;
556
+ isControlled : boolean ;
515
557
}
516
558
}
0 commit comments