@samitouri / QOS-React / commits / d04d03e470

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