Fix a crash when using dynamic children in <option> tag (#13261)

* Make option children a text content by default

fix #11911

* Apply requested changes

- Remove meaningless comments
- revert scripts/rollup/results.json

* remove empty row

* Update comment

* Add a simple unit-test

* [WIP: no flow] Pass through hostContext

* [WIP: no flow] Give better description for test

* Fixes

* Don't pass hostContext through

It ended up being more complicated than I thought.

* Also warn on hydration
This commit is contained in:
Konstantin Yakushin 2018-08-01 18:16:34 +03:00 committed by Dan Abramov
parent 2a2ef7e0fd
commit 0182a74632
7 changed files with 102 additions and 7 deletions

View File

@ -158,6 +158,31 @@ class SelectFixture extends React.Component {
</form>
</div>
</TestCase>
<TestCase
title="An option which contains conditional render fails"
relatedIssues="11911">
<TestCase.Steps>
<li>Select any option</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
Option should be set
</TestCase.ExpectedResult>
<div className="test-fixture">
<select value={this.state.value} onChange={this.onChange}>
<option value="red">
red {this.state.value === 'red' && 'is chosen '} TextNode
</option>
<option value="blue">
blue {this.state.value === 'blue' && 'is chosen '} TextNode
</option>
<option value="green">
green {this.state.value === 'green' && 'is chosen '} TextNode
</option>
</select>
</div>
</TestCase>
</FixtureSet>
);
}

View File

@ -41,9 +41,7 @@ describe('ReactDOMOption', () => {
expect(() => {
node = ReactTestUtils.renderIntoDocument(el);
}).toWarnDev(
'<div> cannot appear as a child of <option>.\n' +
' in div (at **)\n' +
' in option (at **)',
'<div> cannot appear as a child of <option>.\n' + ' in option (at **)',
);
expect(node.innerHTML).toBe('1 2');
ReactTestUtils.renderIntoDocument(el);

View File

@ -545,6 +545,38 @@ describe('ReactDOMSelect', () => {
expect(node.options[2].selected).toBe(false); // gorilla
});
it('should support options with dynamic children', () => {
const container = document.createElement('div');
let node;
function App({value}) {
return (
<select value={value} ref={n => (node = n)} onChange={noop}>
<option key="monkey" value="monkey">
A monkey {value === 'monkey' ? 'is chosen' : null}!
</option>
<option key="giraffe" value="giraffe">
A giraffe {value === 'giraffe' && 'is chosen'}!
</option>
<option key="gorilla" value="gorilla">
A gorilla {value === 'gorilla' && 'is chosen'}!
</option>
</select>
);
}
ReactDOM.render(<App value="monkey" />, container);
expect(node.options[0].selected).toBe(true); // monkey
expect(node.options[1].selected).toBe(false); // giraffe
expect(node.options[2].selected).toBe(false); // gorilla
ReactDOM.render(<App value="giraffe" />, container);
expect(node.options[0].selected).toBe(false); // monkey
expect(node.options[1].selected).toBe(true); // giraffe
expect(node.options[2].selected).toBe(false); // gorilla
});
it('should warn if value is null', () => {
expect(() =>
ReactTestUtils.renderIntoDocument(

View File

@ -413,6 +413,21 @@ describe('ReactDOMServerIntegration', () => {
expectSelectValue(e, 'bar');
},
);
itRenders('an option with flattened children', async render => {
const e = await render(
<select readOnly={true} value="bar">
<option value="bar">
{['Bar', false, 'Foo', <div key="1" />, 'Baz']}
</option>
</select>,
1,
);
expect(e.getAttribute('value')).toBe(null);
expect(e.getAttribute('defaultValue')).toBe(null);
expect(e.firstChild.innerHTML).toBe('BarFooBaz');
expect(e.firstChild.selected).toBe(true);
});
});
describe('user interaction', function() {

View File

@ -58,7 +58,7 @@ module.exports = function(initModules) {
if (console.error.calls.count() > 0) {
console.log(`We saw these warnings:`);
for (let i = 0; i < console.error.calls.count(); i++) {
console.log(console.error.calls.argsFor(i)[0]);
console.log(...console.error.calls.argsFor(i));
}
}
}

View File

@ -9,6 +9,7 @@
import React from 'react';
import warning from 'shared/warning';
import validateDOMNesting from './validateDOMNesting';
let didWarnSelectedSetOnOption = false;
@ -17,8 +18,6 @@ function flattenChildren(children) {
// Flatten children and warn if they aren't strings or numbers;
// invalid types are ignored.
// We can silently skip them because invalid DOM nesting warning
// catches these cases in Fiber.
React.Children.forEach(children, function(child) {
if (child == null) {
return;
@ -26,6 +25,9 @@ function flattenChildren(children) {
if (typeof child === 'string' || typeof child === 'number') {
content += child;
}
// Note: we don't warn about invalid children here.
// Instead, this is done separately below so that
// it happens during the hydration codepath too.
});
return content;
@ -36,8 +38,30 @@ function flattenChildren(children) {
*/
export function validateProps(element: Element, props: Object) {
// TODO (yungsters): Remove support for `selected` in <option>.
if (__DEV__) {
// Warn about invalid children, mirroring the logic above.
if (typeof props.children === 'object' && props.children !== null) {
React.Children.forEach(props.children, function(child) {
if (child == null) {
return;
}
if (typeof child === 'string' || typeof child === 'number') {
return;
}
// This is not real ancestor info but it's close enough
// to produce a useful warning for invalid children.
// We don't have access to the real one because the <option>
// fiber has already been popped, and threading it through
// is needlessly annoying.
const ancestorInfo = validateDOMNesting.updatedAncestorInfo(
null,
'option',
);
validateDOMNesting(child.type, null, ancestorInfo);
});
}
// TODO: Remove support for `selected` in <option>.
if (props.selected != null && !didWarnSelectedSetOnOption) {
warning(
false,

View File

@ -248,6 +248,7 @@ export function prepareUpdate(
export function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'option' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&