diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 03eed7bc1ec65..30ca0f5e8bdaa 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -1433,6 +1433,40 @@ describe('ReactDOMInput', () => { expect(node.getAttribute('value')).toBe('2'); }); + it('does not set the value attribute on password inputs', () => { + const Input = getTestInput(); + const stub = ReactTestUtils.renderIntoDocument( + , + ); + const node = ReactDOM.findDOMNode(stub); + + expect(node.hasAttribute('value')).toBe(false); + expect(node.value).toBe('1'); + }); + + it('does not update the value attribute on password inputs', () => { + const Input = getTestInput(); + const stub = ReactTestUtils.renderIntoDocument( + , + ); + const node = ReactDOM.findDOMNode(stub); + + ReactTestUtils.Simulate.change(node, {target: {value: '2'}}); + + expect(node.hasAttribute('value')).toBe(false); + expect(node.value).toBe('2'); + }); + + it('does not set the defaultValue attribute on password inputs', () => { + const stub = ReactTestUtils.renderIntoDocument( + , + ); + const node = ReactDOM.findDOMNode(stub); + + expect(node.hasAttribute('value')).toBe(false); + expect(node.value).toBe('1'); + }); + it('does not set the value attribute on number inputs if focused', () => { const Input = getTestInput(); const stub = ReactTestUtils.renderIntoDocument( diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js index fc4594261eba2..83a008e0f04f3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js @@ -96,6 +96,50 @@ describe('ReactDOMServerIntegration', () => { expect(e.getAttribute('defaultValue')).toBe(null); }, ); + + itClientRenders( + 'a password input hydrates client-side with the value prop', + async render => { + const e = await render( + , + 0, + ); + + expect(e.value).toBe('foo'); + expect(e.hasAttribute('value')).toBe(false); + }, + ); + + itClientRenders( + 'a password input hydrates client-side with the defaultValue prop', + async render => { + const e = await render( + , + 0, + ); + + expect(e.value).toBe('foo'); + expect(e.hasAttribute('value')).toBe(false); + }, + ); + + it('will not render the value prop server-side', async () => { + const e = await serverRender( + , + ); + + expect(e.value).toBe(''); + expect(e.hasAttribute('value')).toBe(false); + }); + + it('will not render the defaultValue prop server-side', async () => { + const e = await serverRender( + , + ); + + expect(e.value).toBe(''); + expect(e.hasAttribute('value')).toBe(false); + }); }); describe('checkboxes', function() { @@ -540,6 +584,20 @@ describe('ReactDOMServerIntegration', () => { , )); + it('should not blow away user-entered text on successful reconnect to an uncontrolled password input', () => + testUserInteractionBeforeClientRender( + , + '', + 'Hello', + )); + + it('should not blow away user-entered text on successful reconnect to an controlled password input', () => + testUserInteractionBeforeClientRender( + , + '', + 'Hello', + )); + it('should not blow away user-entered text on successful reconnect to a controlled input', async () => { let changeCount = 0; await testUserInteractionBeforeClientRender( diff --git a/packages/react-dom/src/client/ReactDOMFiberInput.js b/packages/react-dom/src/client/ReactDOMFiberInput.js index fd5a574fec377..28c5d7e87db62 100644 --- a/packages/react-dom/src/client/ReactDOMFiberInput.js +++ b/packages/react-dom/src/client/ReactDOMFiberInput.js @@ -218,7 +218,9 @@ export function postMountWrapper(element: Element, props: Object) { // 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 = '' + node._wrapperState.initialValue; + if (props.type !== 'password') { + node.defaultValue = '' + node._wrapperState.initialValue; + } } // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug @@ -304,16 +306,24 @@ export function setDefaultValue( type: ?string, value: *, ) { - if ( - // Focused number inputs synchronize on blur. See ChangeEventPlugin.js - type !== 'number' || - node.ownerDocument.activeElement !== node - ) { - if (value == null) { - node.defaultValue = '' + node._wrapperState.initialValue; - } else if (node.defaultValue !== '' + value) { - node.defaultValue = '' + value; - } + if (type === 'password') { + // Do not synchronize password inputs to prevent password from + // being exposed in markup + // https://github.com/facebook/react/issues/11896 + return; + } + + // Only assign the value attribute on number inputs when they are not selected + // This avoids edge cases in Chrome. Number inputs are synchronized on blur. + // See ChangeEventPlugin.js + if (type === 'number' && node.ownerDocument.activeElement === node) { + return; + } + + if (value == null) { + node.defaultValue = '' + node._wrapperState.initialValue; + } else if (node.defaultValue !== '' + value) { + node.defaultValue = '' + value; } } diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index ec26eba2c1d27..062a23691e698 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -326,6 +326,7 @@ function createOpenTagMarkup( isRootElement: boolean, ): string { let ret = '<' + tagVerbatim; + const isPasswordInput = tagLowercase === 'input' && props.type === 'password'; for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { @@ -335,6 +336,13 @@ function createOpenTagMarkup( if (propValue == null) { continue; } + // Do not render values for password inputs + if ( + isPasswordInput && + (propKey === 'value' || propKey === 'defaultValue') + ) { + continue; + } if (propKey === STYLE) { propValue = createMarkupForStyles(propValue); }