main
js 183 lines 5.71 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import isArray from 'shared/isArray';
11
12 import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
13 import {getToStringValue, toString} from './ToStringValue';
14 import {disableTextareaChildren} from 'shared/ReactFeatureFlags';
15
16 import {track, trackHydrated} from './inputValueTracking';
17 import {queueChangeEvent} from '../events/ReactDOMEventReplaying';
18
19 let didWarnValDefaultVal = false;
20
21 /**
22 * Implements a <textarea> host component that allows setting `value`, and
23 * `defaultValue`. This differs from the traditional DOM API because value is
24 * usually set as PCDATA children.
25 *
26 * If `value` is not supplied (or null/undefined), user actions that affect the
27 * value will trigger updates to the element.
28 *
29 * If `value` is supplied (and not null/undefined), the rendered element will
30 * not trigger updates to the element. Instead, the `value` prop must change in
31 * order for the rendered element to be updated.
32 *
33 * The rendered element will be initialized with an empty value, the prop
34 * `defaultValue` if specified, or the children content (deprecated).
35 */
36
37 export function validateTextareaProps(element: Element, props: Object) {
38 if (__DEV__) {
39 if (
40 props.value !== undefined &&
41 props.defaultValue !== undefined &&
42 !didWarnValDefaultVal
43 ) {
44 console.error(
45 '%s contains a textarea with both value and defaultValue props. ' +
46 'Textarea elements must be either controlled or uncontrolled ' +
47 '(specify either the value prop, or the defaultValue prop, but not ' +
48 'both). Decide between using a controlled or uncontrolled textarea ' +
49 'and remove one of these props. More info: ' +
50 'https://react.dev/link/controlled-components',
51 getCurrentFiberOwnerNameInDevOrNull() || 'A component',
52 );
53 didWarnValDefaultVal = true;
54 }
55 if (props.children != null && props.value == null) {
56 console.error(
57 'Use the `defaultValue` or `value` props instead of setting ' +
58 'children on <textarea>.',
59 );
60 }
61 }
62 }
63
64 export function updateTextarea(
65 element: Element,
66 value: ?string,
67 defaultValue: ?string,
68 ) {
69 const node: HTMLTextAreaElement = element as any;
70 if (value != null) {
71 // Cast `value` to a string to ensure the value is set correctly. While
72 // browsers typically do this as necessary, jsdom doesn't.
73 const newValue = toString(getToStringValue(value));
74 // To avoid side effects (such as losing text selection), only set value if changed
75 if (newValue !== node.value) {
76 node.value = newValue;
77 }
78 // TOOO: This should respect disableInputAttributeSyncing flag.
79 if (defaultValue == null) {
80 if (node.defaultValue !== newValue) {
81 node.defaultValue = newValue;
82 }
83 return;
84 }
85 }
86 if (defaultValue != null) {
87 node.defaultValue = toString(getToStringValue(defaultValue));
88 } else {
89 node.defaultValue = '';
90 }
91 }
92
93 export function initTextarea(
94 element: Element,
95 value: ?string,
96 defaultValue: ?string,
97 children: ?string,
98 ) {
99 const node: HTMLTextAreaElement = element as any;
100
101 let initialValue = value;
102
103 // Only bother fetching default value if we're going to use it
104 if (initialValue == null) {
105 if (children != null) {
106 if (!disableTextareaChildren) {
107 if (defaultValue != null) {
108 throw new Error(
109 'If you supply `defaultValue` on a <textarea>, do not pass children.',
110 );
111 }
112
113 if (isArray(children)) {
114 if (children.length > 1) {
115 throw new Error('<textarea> can only have at most one child.');
116 }
117
118 children = children[0];
119 }
120
121 defaultValue = children;
122 }
123 }
124 if (defaultValue == null) {
125 defaultValue = '';
126 }
127 initialValue = defaultValue;
128 }
129
130 const stringValue = getToStringValue(initialValue);
131 node.defaultValue = stringValue as any; // This will be toString:ed.
132
133 // This is in postMount because we need access to the DOM node, which is not
134 // available until after the component has mounted.
135 const textContent = node.textContent;
136
137 // Only set node.value if textContent is equal to the expected
138 // initial value. In IE10/IE11 there is a bug where the placeholder attribute
139 // will populate textContent as well.
140 // https://developer.microsoft.com/microsoft-edge/platform/issues/101525/
141 // $FlowFixMe[invalid-compare]
142 if (textContent === stringValue) {
143 // $FlowFixMe[invalid-compare]
144 if (textContent !== '' && textContent !== null) {
145 node.value = textContent;
146 }
147 }
148
149 track(element as any);
150 }
151
152 export function hydrateTextarea(
153 element: Element,
154 value: ?string,
155 defaultValue: ?string,
156 ): void {
157 const node: HTMLTextAreaElement = element as any;
158 let initialValue = value;
159 if (initialValue == null) {
160 if (defaultValue == null) {
161 defaultValue = '';
162 }
163 initialValue = defaultValue;
164 }
165 // Track the value that we last observed which is the hydrated value so
166 // that any change event that fires will trigger onChange on the actual
167 // current value.
168 const stringValue = toString(getToStringValue(initialValue));
169 const changed = trackHydrated(node as any, stringValue, false);
170 if (changed) {
171 // If the current value is different, that suggests that the user
172 // changed it before hydration. Queue a replay of the change event.
173 queueChangeEvent(node);
174 }
175 }
176
177 export function restoreControlledTextareaState(
178 element: Element,
179 props: Object,
180 ) {
181 // DOM component is still mounted; update
182 updateTextarea(element, props.value, props.defaultValue);
183 }