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,