Fix passing symbols and functions to textarea (#13362)
* refactor: move getSafeValue to separate file * fix(?): ReactDOMFiberTextarea sanitization for symbols and functions * tests: add TODOs for warnings * fix: restore accidentally removed test * fix: remove redundant logic for initialValue * refactor: integrate SafeValue typings into textarea * fix: restore stringified newValue for equality check * fix: remove getSafeValue from hostProps * refactor: SafeValue -> ToStringValue * refactor: update TODO comment in test file * refactor: no need to convert children to ToStringValue
Rauno Freiberg committed
Aug 14, 2018 at 17:16 UTC
d04d03e4701192abf95c58ff50683b37db6c6eae
2 files changed
+133
-8
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+124
@@ -20,6 +20,8 @@ describe('ReactDOMTextarea', () => {
20
let renderTextarea;
21
22
beforeEach(() => {
23
+ jest.resetModules();
24
+
25
React = require('react');
26
ReactDOM = require('react-dom');
27
ReactDOMServer = require('react-dom/server');
@@ -423,4 +425,126 @@ describe('ReactDOMTextarea', () => {
425
ReactDOM.unmountComponentAtNode(container);
426
ReactDOM.render(<textarea value={undefined} />, container);
427
});
428
+
429
+ describe('When given a Symbol value', () => {
430
+ it('treats initial Symbol value as an empty string', () => {
431
+ const container = document.createElement('div');
432
+ expect(() =>
433
+ ReactDOM.render(
434
+ <textarea value={Symbol('foobar')} onChange={() => {}} />,
435
+ container,
436
+ ),
437
+ ).toWarnDev('Invalid value for prop `value`');
438
+ const node = container.firstChild;
439
+
440
+ expect(node.value).toBe('');
441
+ });
442
+
443
+ it('treats initial Symbol children as an empty string', () => {
444
+ const container = document.createElement('div');
445
+ expect(() =>
446
+ ReactDOM.render(
447
+ <textarea onChange={() => {}}>{Symbol('foo')}</textarea>,
448
+ container,
449
+ ),
450
+ ).toWarnDev('Use the `defaultValue` or `value` props');
451
+ const node = container.firstChild;
452
+
453
+ expect(node.value).toBe('');
454
+ });
455
+
456
+ it('treats updated Symbol value as an empty string', () => {
457
+ const container = document.createElement('div');
458
+ ReactDOM.render(<textarea value="foo" onChange={() => {}} />, container);
459
+ expect(() =>
460
+ ReactDOM.render(
461
+ <textarea value={Symbol('foo')} onChange={() => {}} />,
462
+ container,
463
+ ),
464
+ ).toWarnDev('Invalid value for prop `value`');
465
+ const node = container.firstChild;
466
+
467
+ expect(node.value).toBe('');
468
+ });
469
+
470
+ it('treats initial Symbol defaultValue as an empty string', () => {
471
+ const container = document.createElement('div');
472
+ ReactDOM.render(<textarea defaultValue={Symbol('foobar')} />, container);
473
+ const node = container.firstChild;
474
+
475
+ // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
476
+ expect(node.value).toBe('');
477
+ });
478
+
479
+ it('treats updated Symbol defaultValue as an empty string', () => {
480
+ const container = document.createElement('div');
481
+ ReactDOM.render(<textarea defaultValue="foo" />, container);
482
+ ReactDOM.render(<textarea defaultValue={Symbol('foobar')} />, container);
483
+ const node = container.firstChild;
484
+
485
+ // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
486
+ expect(node.value).toBe('foo');
487
+ });
488
+ });
489
+
490
+ describe('When given a function value', () => {
491
+ it('treats initial function value as an empty string', () => {
492
+ const container = document.createElement('div');
493
+ expect(() =>
494
+ ReactDOM.render(
495
+ <textarea value={() => {}} onChange={() => {}} />,
496
+ container,
497
+ ),
498
+ ).toWarnDev('Invalid value for prop `value`');
499
+ const node = container.firstChild;
500
+
501
+ expect(node.value).toBe('');
502
+ });
503
+
504
+ it('treats initial function children as an empty string', () => {
505
+ const container = document.createElement('div');
506
+ expect(() =>
507
+ ReactDOM.render(
508
+ <textarea onChange={() => {}}>{() => {}}</textarea>,
509
+ container,
510
+ ),
511
+ ).toWarnDev('Use the `defaultValue` or `value` props');
512
+ const node = container.firstChild;
513
+
514
+ expect(node.value).toBe('');
515
+ });
516
+
517
+ it('treats updated function value as an empty string', () => {
518
+ const container = document.createElement('div');
519
+ ReactDOM.render(<textarea value="foo" onChange={() => {}} />, container);
520
+ expect(() =>
521
+ ReactDOM.render(
522
+ <textarea value={() => {}} onChange={() => {}} />,
523
+ container,
524
+ ),
525
+ ).toWarnDev('Invalid value for prop `value`');
526
+ const node = container.firstChild;
527
+
528
+ expect(node.value).toBe('');
529
+ });
530
+
531
+ it('treats initial function defaultValue as an empty string', () => {
532
+ const container = document.createElement('div');
533
+ ReactDOM.render(<textarea defaultValue={() => {}} />, container);
534
+ const node = container.firstChild;
535
+
536
+ // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
537
+ expect(node.value).toBe('');
538
+ });
539
+
540
+ it('treats updated function defaultValue as an empty string', () => {
541
+ const container = document.createElement('div');
542
+ ReactDOM.render(<textarea defaultValue="foo" />, container);
543
+ ReactDOM.render(<textarea defaultValue={() => {}} />, container);
544
+ const node = container.firstChild;
545
+
546
+ // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
547
+ expect(node.value).toBe('foo');
548
+ });
549
+ });
550
});
packages/react-dom/src/client/ReactDOMFiberTextarea.js
+9
-8
@@ -12,12 +12,14 @@ import warning from 'shared/warning';
12
13
import ReactControlledValuePropTypes from '../shared/ReactControlledValuePropTypes';
14
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
15
+import {getToStringValue, toString} from './ToStringValue';
16
+import type {ToStringValue} from './ToStringValue';
17
18
let didWarnValDefaultVal = false;
19
20
type TextAreaWithWrapperState = HTMLTextAreaElement & {
21
_wrapperState: {
20
- initialValue: string,
22
+ initialValue: ToStringValue,
23
},
24
};
25
@@ -54,7 +56,7 @@ export function getHostProps(element: Element, props: Object) {
56
...props,
57
value: undefined,
58
defaultValue: undefined,
57
- children: '' + node._wrapperState.initialValue,
59
+ children: toString(node._wrapperState.initialValue),
60
};
61
62
return hostProps;
@@ -110,7 +112,7 @@ export function initWrapperState(element: Element, props: Object) {
112
children = children[0];
113
}
114
113
- defaultValue = '' + children;
115
+ defaultValue = children;
116
}
117
if (defaultValue == null) {
118
defaultValue = '';
@@ -119,18 +121,17 @@ export function initWrapperState(element: Element, props: Object) {
121
}
122
123
node._wrapperState = {
122
- initialValue: '' + initialValue,
124
+ initialValue: getToStringValue(initialValue),
125
};
126
}
127
128
export function updateWrapper(element: Element, props: Object) {
129
const node = ((element: any): TextAreaWithWrapperState);
128
- const value = props.value;
130
+ const value = getToStringValue(props.value);
131
if (value != null) {
132
// Cast `value` to a string to ensure the value is set correctly. While
133
// browsers typically do this as necessary, jsdom doesn't.
132
- const newValue = '' + value;
133
-
134
+ const newValue = toString(value);
135
// To avoid side effects (such as losing text selection), only set value if changed
136
if (newValue !== node.value) {
137
node.value = newValue;
@@ -140,7 +141,7 @@ export function updateWrapper(element: Element, props: Object) {
141
}
142
}
143
if (props.defaultValue != null) {
143
- node.defaultValue = props.defaultValue;
144
+ node.defaultValue = toString(getToStringValue(props.defaultValue));
145
}
146
}
147