diff --git a/fixtures/dom/src/components/fixtures/number-inputs/index.js b/fixtures/dom/src/components/fixtures/number-inputs/index.js index 99b737912a68b..42f48c005e038 100644 --- a/fixtures/dom/src/components/fixtures/number-inputs/index.js +++ b/fixtures/dom/src/components/fixtures/number-inputs/index.js @@ -27,7 +27,7 @@ function NumberInputs() {

- Notes: Chrome and Safari clear trailing decimals on blur. React + Notes: Modern Chrome and Safari {'<='} 6 clear trailing decimals on blur. React makes this concession so that the value attribute remains in sync with the value property.

diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index b75c18c9866a9..e44b9ede8fa81 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -1152,15 +1152,20 @@ describe('ReactDOMInput', () => { var originalCreateElement = document.createElement; spyOn(document, 'createElement').and.callFake(function(type) { var el = originalCreateElement.apply(this, arguments); + var value = ''; + if (type === 'input') { Object.defineProperty(el, 'value', { - get: function() {}, - set: function() { - log.push('set value'); + get: function() { + return value; + }, + set: function(val) { + value = '' + val; + log.push('set property value'); }, }); spyOn(el, 'setAttribute').and.callFake(function(name, value) { - log.push('set ' + name); + log.push('set attribute ' + name); }); } return el; @@ -1170,14 +1175,14 @@ describe('ReactDOMInput', () => { , ); expect(log).toEqual([ - 'set type', - 'set step', - 'set min', - 'set max', - 'set value', - 'set value', - 'set checked', - 'set checked', + 'set attribute type', + 'set attribute min', + 'set attribute max', + 'set attribute step', + 'set attribute value', + 'set property value', + 'set attribute checked', + 'set attribute checked', ]); }); @@ -1216,9 +1221,14 @@ describe('ReactDOMInput', () => { var originalCreateElement = document.createElement; spyOn(document, 'createElement').and.callFake(function(type) { var el = originalCreateElement.apply(this, arguments); + var value = ''; if (type === 'input') { Object.defineProperty(el, 'value', { + get: function() { + return value; + }, set: function(val) { + value = '' + val; log.push(`node.value = ${strify(val)}`); }, }); @@ -1235,8 +1245,7 @@ describe('ReactDOMInput', () => { expect(log).toEqual([ 'node.setAttribute("type", "date")', 'node.setAttribute("value", "1980-01-01")', - 'node.value = ""', - 'node.value = ""', + 'node.value = "1980-01-01"', 'node.setAttribute("checked", "")', 'node.setAttribute("checked", "")', ]); @@ -1270,6 +1279,36 @@ describe('ReactDOMInput', () => { expect(node.getAttribute('value')).toBe('2'); }); + it('initially sets the value attribute on mount', () => { + var Input = getTestInput(); + var stub = ReactTestUtils.renderIntoDocument( + , + ); + var node = ReactDOM.findDOMNode(stub); + + expect(node.getAttribute('value')).toBe('1'); + }); + + it('initially sets the value attribute for submit on mount', () => { + var Input = getTestInput(); + var stub = ReactTestUtils.renderIntoDocument( + , + ); + var node = ReactDOM.findDOMNode(stub); + + expect(node.getAttribute('value')).toBe('1'); + }); + + it('initially sets the value attribute for reset on mount', () => { + var Input = getTestInput(); + var stub = ReactTestUtils.renderIntoDocument( + , + ); + var node = ReactDOM.findDOMNode(stub); + + expect(node.getAttribute('value')).toBe('1'); + }); + it('does not set the value attribute on number inputs if focused', () => { var Input = getTestInput(); var stub = ReactTestUtils.renderIntoDocument( diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js index 309ef65350be9..51ba953d2dfae 100644 --- a/packages/react-dom/src/client/DOMPropertyOperations.js +++ b/packages/react-dom/src/client/DOMPropertyOperations.js @@ -73,8 +73,7 @@ export function getValueForProperty(node, name, expected) { if (__DEV__) { var propertyInfo = getPropertyInfo(name); if (propertyInfo) { - var mutationMethod = propertyInfo.mutationMethod; - if (mutationMethod || propertyInfo.mustUseProperty) { + if (propertyInfo.mustUseProperty) { return node[propertyInfo.propertyName]; } else { var attributeName = propertyInfo.attributeName; @@ -157,10 +156,7 @@ export function setValueForProperty(node, name, value) { var propertyInfo = getPropertyInfo(name); if (propertyInfo && shouldSetAttribute(name, value)) { - var mutationMethod = propertyInfo.mutationMethod; - if (mutationMethod) { - mutationMethod(node, value); - } else if (shouldIgnoreValue(propertyInfo, value)) { + if (shouldIgnoreValue(propertyInfo, value)) { deleteValueForProperty(node, name); return; } else if (propertyInfo.mustUseProperty) { @@ -233,10 +229,7 @@ export function deleteValueForAttribute(node, name) { export function deleteValueForProperty(node, name) { var propertyInfo = getPropertyInfo(name); if (propertyInfo) { - var mutationMethod = propertyInfo.mutationMethod; - if (mutationMethod) { - mutationMethod(node, undefined); - } else if (propertyInfo.mustUseProperty) { + if (propertyInfo.mustUseProperty) { var propName = propertyInfo.propertyName; if (propertyInfo.hasBooleanValue) { node[propName] = false; diff --git a/packages/react-dom/src/client/ReactDOMFiberInput.js b/packages/react-dom/src/client/ReactDOMFiberInput.js index cc1425deee6b6..410a12e2397cb 100644 --- a/packages/react-dom/src/client/ReactDOMFiberInput.js +++ b/packages/react-dom/src/client/ReactDOMFiberInput.js @@ -57,30 +57,14 @@ function isControlled(props) { export function getHostProps(element: Element, props: Object) { var node = ((element: any): InputWithWrapperState); - var value = props.value; var checked = props.checked; - var hostProps = Object.assign( - { - // Make sure we set .type before any other properties (setting .value - // before .type means .value is lost in IE11 and below) - type: undefined, - // Make sure we set .step before .value (setting .value before .step - // means .value is rounded on mount, based upon step precision) - step: undefined, - // Make sure we set .min & .max before .value (to ensure proper order - // in corner cases such as min or max deriving from value, e.g. Issue #7170) - min: undefined, - max: undefined, - }, - props, - { - defaultChecked: undefined, - defaultValue: undefined, - value: value != null ? value : node._wrapperState.initialValue, - checked: checked != null ? checked : node._wrapperState.initialChecked, - }, - ); + var hostProps = Object.assign({}, props, { + defaultChecked: undefined, + defaultValue: undefined, + value: undefined, + checked: checked != null ? checked : node._wrapperState.initialChecked, + }); return hostProps; } @@ -131,7 +115,7 @@ export function initWrapperState(element: Element, props: Object) { } } - var defaultValue = props.defaultValue; + var defaultValue = props.defaultValue == null ? '' : props.defaultValue; var node = ((element: any): InputWithWrapperState); node._wrapperState = { initialChecked: @@ -190,6 +174,7 @@ export function updateWrapper(element: Element, props: Object) { } var value = props.value; + var valueAsString = '' + props.value; if (value != null) { if (value === 0 && node.value === '') { node.value = '0'; @@ -206,26 +191,17 @@ export function updateWrapper(element: Element, props: Object) { ) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - node.value = '' + value; + node.value = valueAsString; } - } else if (node.value !== '' + value) { + } else if (node.value !== valueAsString) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - node.value = '' + value; + node.value = valueAsString; } + synchronizeDefaultValue(node, props.type, valueAsString); } else { if (props.value == null && props.defaultValue != null) { - // 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 - if (node.defaultValue !== '' + props.defaultValue) { - node.defaultValue = '' + props.defaultValue; - } + synchronizeDefaultValue(node, props.type, '' + props.defaultValue); } if (props.checked == null && props.defaultChecked != null) { node.defaultChecked = !!props.defaultChecked; @@ -235,32 +211,20 @@ export function updateWrapper(element: Element, props: Object) { export function postMountWrapper(element: Element, props: Object) { var node = ((element: any): InputWithWrapperState); + var hasUserInput = node.value !== ''; + var value = node._wrapperState.initialValue; - // Detach value from defaultValue. We won't do anything if we're working on - // submit or reset inputs as those values & defaultValues are linked. They - // are not resetable nodes so this operation doesn't matter and actually - // removes browser-default values (eg "Submit Query") when no value is - // provided. + if (value !== '' || props.hasOwnProperty('value')) { + // Do not assign value if it is already set. This prevents user text input + // from being lost during SSR hydration. + if (!hasUserInput) { + node.value = value; + } - switch (props.type) { - case 'submit': - case 'reset': - break; - case 'color': - case 'date': - case 'datetime': - case 'datetime-local': - case 'month': - case 'time': - case 'week': - // This fixes the no-show issue on iOS Safari and Android Chrome: - // https://github.com/facebook/react/issues/7233 - node.value = ''; - node.value = node.defaultValue; - break; - default: - node.value = node.value; - break; + // value must be assigned before defaultValue. This fixes an issue where the + // visually displayed value of date inputs disappears on mobile Safari and Chrome: + // https://github.com/facebook/react/issues/7233 + node.defaultValue = value; } // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug @@ -327,3 +291,21 @@ function updateNamedCousins(rootNode, props) { } } } + +// 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 +function synchronizeDefaultValue(node: Element, type: ?string, value: string) { + if ( + // Focused number inputs synchronize on blur. See ChangeEventPlugin.js + (type !== 'number' || node.ownerDocument.activeElement !== node) && + node.defaultValue !== value + ) { + node.defaultValue = value; + } +} diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js index 5801f4428b1e2..3bc764b16dd81 100644 --- a/packages/react-dom/src/shared/DOMProperty.js +++ b/packages/react-dom/src/shared/DOMProperty.js @@ -83,7 +83,6 @@ var DOMPropertyInjection = { attributeName: lowerCased, attributeNamespace: null, propertyName: propName, - mutationMethod: null, mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY), hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE), @@ -121,10 +120,6 @@ var DOMPropertyInjection = { propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName]; } - if (DOMMutationMethods.hasOwnProperty(propName)) { - propertyInfo.mutationMethod = DOMMutationMethods[propName]; - } - // Downcase references to whitelist properties to check for membership // without case-sensitivity. This allows the whitelist to pick up // `allowfullscreen`, which should be written using the property configuration @@ -154,9 +149,6 @@ export const ROOT_ATTRIBUTE_NAME = 'data-reactroot'; * propertyName: * Used on DOM node instances. (This includes properties that mutate due to * external factors.) - * mutationMethod: - * If non-null, used instead of the property or `setAttribute()` after - * initial render. * mustUseProperty: * Whether the property must be accessed and mutated as an object property. * hasBooleanValue: diff --git a/packages/react-dom/src/shared/HTMLDOMPropertyConfig.js b/packages/react-dom/src/shared/HTMLDOMPropertyConfig.js index d2da706a4432b..c3272005a2a34 100644 --- a/packages/react-dom/src/shared/HTMLDOMPropertyConfig.js +++ b/packages/react-dom/src/shared/HTMLDOMPropertyConfig.js @@ -83,34 +83,6 @@ var HTMLDOMPropertyConfig = { htmlFor: 'for', httpEquiv: 'http-equiv', }, - DOMMutationMethods: { - value: function(node, value) { - if (value == null) { - return node.removeAttribute('value'); - } - - // Number inputs get special treatment due to some edge cases in - // Chrome. Let everything else assign the value attribute as normal. - // https://github.com/facebook/react/issues/7253#issuecomment-236074326 - if (node.type !== 'number' || node.hasAttribute('value') === false) { - node.setAttribute('value', '' + value); - } else if ( - node.validity && - !node.validity.badInput && - node.ownerDocument.activeElement !== node - ) { - // Don't assign an attribute if validation reports bad - // input. Chrome will clear the value. Additionally, don't - // operate on inputs that have focus, otherwise Chrome might - // strip off trailing decimal places and cause the user's - // cursor position to jump to the beginning of the input. - // - // In ReactDOMInput, we have an onBlur event that will trigger - // this function again when focus is lost. - node.setAttribute('value', '' + value); - } - }, - }, }; export default HTMLDOMPropertyConfig; diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index a96e8d51fa9d4..dc3d005157dde 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -1,52 +1,52 @@ { "bundleSizes": { "react.development.js (UMD_DEV)": { - "size": 54798, - "gzip": 14827 + "size": 54852, + "gzip": 14854 }, "react.production.min.js (UMD_PROD)": { "size": 6563, "gzip": 2725 }, "react.development.js (NODE_DEV)": { - "size": 45166, - "gzip": 12533 + "size": 45220, + "gzip": 12560 }, "react.production.min.js (NODE_PROD)": { "size": 5363, "gzip": 2304 }, "React-dev.js (FB_DEV)": { - "size": 45060, - "gzip": 12278 + "size": 45114, + "gzip": 12305 }, "React-prod.js (FB_PROD)": { "size": 12462, "gzip": 3316 }, "react-dom.development.js (UMD_DEV)": { - "size": 566431, - "gzip": 132055 + "size": 565476, + "gzip": 131642 }, "react-dom.production.min.js (UMD_PROD)": { - "size": 94557, - "gzip": 30616 + "size": 94152, + "gzip": 30493 }, "react-dom.development.js (NODE_DEV)": { - "size": 547400, - "gzip": 127133 + "size": 546451, + "gzip": 126758 }, "react-dom.production.min.js (NODE_PROD)": { - "size": 92733, - "gzip": 29845 + "size": 92328, + "gzip": 29689 }, "ReactDOM-dev.js (FB_DEV)": { - "size": 572903, - "gzip": 131672 + "size": 571895, + "gzip": 131240 }, "ReactDOM-prod.js (FB_PROD)": { - "size": 270884, - "gzip": 51317 + "size": 271202, + "gzip": 51374 }, "react-dom-test-utils.development.js (NODE_DEV)": { "size": 35984, @@ -85,96 +85,96 @@ "gzip": 5327 }, "react-dom-server.browser.development.js (UMD_DEV)": { - "size": 94237, - "gzip": 25344 + "size": 92759, + "gzip": 24859 }, "react-dom-server.browser.production.min.js (UMD_PROD)": { - "size": 14726, - "gzip": 5829 + "size": 14371, + "gzip": 5708 }, "react-dom-server.browser.development.js (NODE_DEV)": { - "size": 83181, - "gzip": 22539 + "size": 81703, + "gzip": 22054 }, "react-dom-server.browser.production.min.js (NODE_PROD)": { - "size": 14201, - "gzip": 5709 + "size": 13846, + "gzip": 5589 }, "ReactDOMServer-dev.js (FB_DEV)": { - "size": 86907, - "gzip": 22791 + "size": 85398, + "gzip": 22314 }, "ReactDOMServer-prod.js (FB_PROD)": { - "size": 31375, - "gzip": 8108 + "size": 30709, + "gzip": 7951 }, "react-dom-server.node.development.js (NODE_DEV)": { - "size": 85163, - "gzip": 23040 + "size": 83685, + "gzip": 22562 }, "react-dom-server.node.production.min.js (NODE_PROD)": { - "size": 15026, - "gzip": 6017 + "size": 14671, + "gzip": 5892 }, "react-art.development.js (UMD_DEV)": { - "size": 356125, - "gzip": 78802 + "size": 357274, + "gzip": 79043 }, "react-art.production.min.js (UMD_PROD)": { - "size": 83543, - "gzip": 25713 + "size": 83595, + "gzip": 25720 }, "react-art.development.js (NODE_DEV)": { - "size": 280504, - "gzip": 59796 + "size": 281659, + "gzip": 60027 }, "react-art.production.min.js (NODE_PROD)": { - "size": 47440, - "gzip": 14827 + "size": 47492, + "gzip": 14835 }, "ReactART-dev.js (FB_DEV)": { - "size": 291930, - "gzip": 61727 + "size": 293097, + "gzip": 61958 }, "ReactART-prod.js (FB_PROD)": { - "size": 146916, - "gzip": 25076 + "size": 147194, + "gzip": 25111 }, "ReactNativeRenderer-dev.js (RN_DEV)": { - "size": 410160, - "gzip": 89957 + "size": 411327, + "gzip": 90189 }, "ReactNativeRenderer-prod.js (RN_PROD)": { - "size": 200633, - "gzip": 34575 + "size": 200911, + "gzip": 34607 }, "ReactRTRenderer-dev.js (RN_DEV)": { - "size": 286366, - "gzip": 60705 + "size": 288665, + "gzip": 61271 }, "ReactRTRenderer-prod.js (RN_PROD)": { - "size": 137987, - "gzip": 23145 + "size": 138829, + "gzip": 23302 }, "ReactCSRenderer-dev.js (RN_DEV)": { - "size": 277219, - "gzip": 57857 + "size": 278386, + "gzip": 58087 }, "ReactCSRenderer-prod.js (RN_PROD)": { - "size": 130874, - "gzip": 21846 + "size": 131152, + "gzip": 21884 }, "react-test-renderer.development.js (NODE_DEV)": { - "size": 278461, - "gzip": 58903 + "size": 279616, + "gzip": 59132 }, "react-test-renderer.production.min.js (NODE_PROD)": { - "size": 46042, - "gzip": 14219 + "size": 46094, + "gzip": 14230 }, "ReactTestRenderer-dev.js (FB_DEV)": { - "size": 289988, - "gzip": 60842 + "size": 291155, + "gzip": 61073 }, "react-test-renderer-shallow.development.js (NODE_DEV)": { "size": 9686, @@ -189,16 +189,16 @@ "gzip": 2596 }, "react-noop-renderer.development.js (NODE_DEV)": { - "size": 274977, - "gzip": 57886 + "size": 276132, + "gzip": 58115 }, "react-reconciler.development.js (NODE_DEV)": { - "size": 260412, - "gzip": 54540 + "size": 261567, + "gzip": 54769 }, "react-reconciler.production.min.js (NODE_PROD)": { - "size": 39420, - "gzip": 12247 + "size": 39472, + "gzip": 12262 }, "react-call-return.development.js (NODE_DEV)": { "size": 2514,