diff --git a/packages/react-dom-bindings/src/client/ReactDOMInput.js b/packages/react-dom-bindings/src/client/ReactDOMInput.js index d38ae2fe0ae..eb825964159 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMInput.js +++ b/packages/react-dom-bindings/src/client/ReactDOMInput.js @@ -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 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); } } diff --git a/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js index c4ec263bb3d..42f246046c8 100644 --- a/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js @@ -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}; diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 04bd96fe2e8..e4de7c98b1b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -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(); @@ -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'); } });