| 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 | } |