[Fiber] Update input.defaultValue for type=number too (#36980)

We used to avoid updating .defaultValue until blur on number inputs
because of a Chrome bug, but the relevant Chromium behavior was fixed in
2020 in M85 https://issues.chromium.org/issues/40124871 and likewise in
WebKit https://bugs.webkit.org/show_bug.cgi?id=217156 so I think we can
probably just remove this special-cased logic.

We do still need some special logic for number inputs because it's the
only type where we let users specify a non-string value that has
multiple string representations and need to be careful not to clobber
the .value if it has a slightly different string than what you'd get
from casting a number to string.

Fixes #29862.

Tested with the number inputs fixture in current Chrome, Safari, and
Firefox.
This commit is contained in:
Sophie Alpert
2026-07-09 19:58:34 -07:00
committed by GitHub
parent de89c17390
commit 3596bf4463
3 changed files with 25 additions and 56 deletions
+23 -26
View File
@@ -13,7 +13,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import {getToStringValue, toString} from './ToStringValue';
import {track, trackHydrated, updateValueIfChanged} from './inputValueTracking';
import getActiveElement from './getActiveElement';
import {
disableInputAttributeSyncing,
enableHydrationChangeEvent,
@@ -121,10 +120,13 @@ export function updateInput(
if (value != null) {
if (type === 'number') {
if (
// "" == 0, so a cleared field wouldn't otherwise be restored to 0.
// $FlowFixMe[incompatible-type]
// $FlowFixMe[invalid-compare]
(value === 0 && node.value === '') ||
// We explicitly want to coerce to number here if possible.
// We explicitly want to coerce to number here if possible, so that
// other spellings of the same number (e.g. "0.0" mid-edit) aren't
// clobbered while the user types.
// eslint-disable-next-line
node.value != (value as any)
) {
@@ -144,7 +146,7 @@ export function updateInput(
// whenever the defaultValue React prop has changed. When not present,
// React does nothing
if (defaultValue != null) {
setDefaultValue(node, type, getToStringValue(defaultValue));
setDefaultValue(node, getToStringValue(defaultValue));
} else if (lastDefaultValue != null) {
node.removeAttribute('value');
}
@@ -155,9 +157,22 @@ export function updateInput(
// 2. The defaultValue React property
// 3. Otherwise there should be no change
if (value != null) {
setDefaultValue(node, type, getToStringValue(value));
if (
type === 'number' &&
// We explicitly want to coerce to number here if possible.
// eslint-disable-next-line
node.value == (value as any)
) {
// node.value may be a different spelling of the same number (e.g.
// "0.0" for 0). Mirror what's displayed, like the value setter does.
// Not redundant with the assignment above: browsers sanitize invalid
// assigned values to "", in which case we sync the React value below.
setDefaultValue(node, getToStringValue(node.value));
} else {
setDefaultValue(node, getToStringValue(value));
}
} else if (defaultValue != null) {
setDefaultValue(node, type, getToStringValue(defaultValue));
setDefaultValue(node, getToStringValue(defaultValue));
} else if (lastDefaultValue != null) {
node.removeAttribute('value');
}
@@ -462,26 +477,8 @@ export function restoreControlledInputState(element: Element, props: Object) {
}
}
// In Chrome, assigning defaultValue to certain input types triggers input validation.
// For number inputs, the display value loses trailing decimal points. For email inputs,
// Chrome raises "The specified value <x> is not a valid email address".
//
// Here we check to see if the defaultValue has actually changed, avoiding these problems
// when the user is inputting text
//
// https://github.com/facebook/react/issues/7253
export function setDefaultValue(
node: HTMLInputElement,
type: ?string,
value: ToStringValue,
) {
if (
// Focused number inputs synchronize on blur. See ChangeEventPlugin.js
type !== 'number' ||
getActiveElement(node.ownerDocument) !== node
) {
if (node.defaultValue !== toString(value)) {
node.defaultValue = toString(value);
}
function setDefaultValue(node: HTMLInputElement, value: ToStringValue) {
if (node.defaultValue !== toString(value)) {
node.defaultValue = toString(value);
}
}
@@ -23,10 +23,8 @@ import getEventTarget from '../getEventTarget';
import isEventSupported from '../isEventSupported';
import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
import {updateValueIfChanged} from '../../client/inputValueTracking';
import {setDefaultValue} from '../../client/ReactDOMInput';
import {enqueueStateRestore} from '../ReactDOMControlledComponent';
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
import {batchedUpdates} from '../ReactDOMUpdateBatching';
import {
processDispatchQueue,
@@ -260,20 +258,6 @@ function getTargetInstForInputOrChangeEvent(
}
}
function handleControlledInputBlur(node: HTMLInputElement, props: any) {
if (node.type !== 'number') {
return;
}
if (!disableInputAttributeSyncing) {
const isControlled = props.value != null;
if (isControlled) {
// If controlled, assign the value attribute to the current value on blur
setDefaultValue(node as any, 'number', (node as any).value);
}
}
}
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
@@ -330,15 +314,6 @@ function extractEvents(
if (handleEventFunc) {
handleEventFunc(domEventName, targetNode, targetInst);
}
// When blurring, set the value attribute for number inputs
if (domEventName === 'focusout' && targetInst) {
// These props aren't necessarily the most current but we warn for changing
// between controlled and uncontrolled, so it doesn't matter and the previous
// code was also broken for changes.
const props = targetInst.memoizedProps;
handleControlledInputBlur(targetNode as any as HTMLInputElement, props);
}
}
export {registerEvents, extractEvents};
+2 -5
View File
@@ -1035,9 +1035,6 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('0.0');
expect(node.hasAttribute('value')).toBe(false);
} else {
dispatchEventOnNode(node, 'blur');
dispatchEventOnNode(node, 'focusout');
expect(node.value).toBe('0.0');
expect(node.getAttribute('value')).toBe('0.0');
}
@@ -2664,7 +2661,7 @@ describe('ReactDOMInput', () => {
}
});
it('does not set the value attribute on number inputs if focused', async () => {
it('sets the value attribute on number inputs even when focused', async () => {
const Input = getTestInput();
await act(() => {
root.render(<Input type="number" value="1" />);
@@ -2681,7 +2678,7 @@ describe('ReactDOMInput', () => {
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('1');
expect(node.getAttribute('value')).toBe('2');
}
});