diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index 1446c2d3909..cad08f0f9e8 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -29,8 +29,7 @@ void AndroidTextInputShadowNode::setContextContainer( contextContainer_ = contextContainer; } -AttributedString AndroidTextInputShadowNode::getAttributedString( - bool usePlaceholders) const { +AttributedString AndroidTextInputShadowNode::getAttributedString() const { // Use BaseTextShadowNode to get attributed string from children auto childTextAttributes = TextAttributes::defaultTextAttributes(); childTextAttributes.apply(getProps()->textAttributes); @@ -61,16 +60,21 @@ AttributedString AndroidTextInputShadowNode::getAttributedString( return attributedString; } + return getPlaceholderAttributedString(false); +} + +// For measurement purposes, we want to make sure that there's at least a +// single character in the string so that the measured height is greater +// than zero. Otherwise, empty TextInputs with no placeholder don't +// display at all. +AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString( + bool ensureMinimumLength) const { // Return placeholder text, since text and children are empty. auto textAttributedString = AttributedString{}; auto fragment = AttributedString::Fragment{}; fragment.string = getProps()->placeholder; - // For measurement purposes, we want to make sure that there's at least a - // single character in the string so that the measured height is greater - // than zero. Otherwise, empty TextInputs with no placeholder don't - // display at all. - if (fragment.string.empty() && usePlaceholders) { + if (fragment.string.empty() && ensureMinimumLength) { fragment.string = " "; } @@ -92,7 +96,7 @@ void AndroidTextInputShadowNode::setTextLayoutManager( void AndroidTextInputShadowNode::updateStateIfNeeded() { ensureUnsealed(); - auto attributedString = getAttributedString(false); + auto reactTreeAttributedString = getAttributedString(); auto const &state = getStateData(); assert(textLayoutManager_); @@ -100,13 +104,17 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { (!state.layoutManager || state.layoutManager == textLayoutManager_) && "`StateData` refers to a different `TextLayoutManager`"); - if (state.attributedString == attributedString && + // Tree is often out of sync with the value of the TextInput. + // This is by design - don't change the value of the TextInput in the State, + // and therefore in Java, unless the tree itself changes. + if (state.reactTreeAttributedString == reactTreeAttributedString && state.layoutManager == textLayoutManager_) { return; } setStateData(AndroidTextInputState{state.mostRecentEventCount, - attributedString, + reactTreeAttributedString, + reactTreeAttributedString, getProps()->paragraphAttributes, textLayoutManager_}); } @@ -115,7 +123,13 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { Size AndroidTextInputShadowNode::measure( LayoutConstraints layoutConstraints) const { - AttributedString attributedString = getAttributedString(true); + auto const &state = getStateData(); + + AttributedString attributedString = state.attributedString; + + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(true); + } if (attributedString.isEmpty()) { return {0, 0}; diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h index 796260c8c85..334ebb06648 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h @@ -37,7 +37,9 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode< /* * Returns a `AttributedString` which represents text content of the node. */ - AttributedString getAttributedString(bool usePlaceholders) const; + AttributedString getAttributedString() const; + AttributedString getPlaceholderAttributedString( + bool ensureMinimumLength) const; /* * Associates a shared TextLayoutManager with the node. diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp index 4a53da92f24..4e37be0aa6f 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp @@ -15,6 +15,7 @@ namespace react { #ifdef ANDROID folly::dynamic AndroidTextInputState::getDynamic() const { + // Java doesn't need all fields, so we don't pass them along. folly::dynamic newState = folly::dynamic::object(); newState["mostRecentEventCount"] = mostRecentEventCount; newState["attributedString"] = toDynamic(attributedString); diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h index 8d356637396..b305fefb654 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h @@ -31,6 +31,15 @@ class AndroidTextInputState final { */ AttributedString attributedString{}; + /* + * All content of component represented as an `AttributedString`. + * This stores the previous computed *from the React tree*. This usually + * doesn't change as the TextInput contents are being updated. If it does + * change, we need to wipe out current contents of the TextInput and replace + * with the new value from the tree. + */ + AttributedString reactTreeAttributedString{}; + /* * Represents all visual attributes of a paragraph of text represented as * a ParagraphAttributes. @@ -79,10 +88,12 @@ class AndroidTextInputState final { AndroidTextInputState( int64_t mostRecentEventCount, AttributedString const &attributedString, + AttributedString const &reactTreeAttributedString, ParagraphAttributes const ¶graphAttributes, SharedTextLayoutManager const &layoutManager) : mostRecentEventCount(mostRecentEventCount), attributedString(attributedString), + reactTreeAttributedString(reactTreeAttributedString), paragraphAttributes(paragraphAttributes), layoutManager(layoutManager) {} AndroidTextInputState() = default; @@ -92,6 +103,7 @@ class AndroidTextInputState final { : mostRecentEventCount((int64_t)data["mostRecentEventCount"].getInt()), attributedString( updateAttributedString(previousState.attributedString, data)), + reactTreeAttributedString(previousState.reactTreeAttributedString), paragraphAttributes(previousState.paragraphAttributes), layoutManager(previousState.layoutManager){}; folly::dynamic getDynamic() const;