From b9491b7c5104066b2714045cd7710f995458c9e9 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 5 Dec 2019 13:16:09 -0800 Subject: [PATCH] TextInput: support editing completely empty TextInputs Summary: Before this change, C++ couldn't propagate changes that updated TextInputs that were completely empty. In C++ the AttributedString cannot contain Fragments with empty text, so a completely empty TextInput would have no Fragments; this breaks the C++ state value updating infra since it relies on copying over existing fragments. Instead, now we propagate default TextAttributes and ShadowView through State, so that State updates can use them to construct new Fragments. Changelog: [Internal] Reviewed By: shergin, mdvacca Differential Revision: D18835048 fbshipit-source-id: 58ac94c5454c8610c6287b096b62199045e5879b --- .../AndroidTextInputShadowNode.cpp | 13 +++++++ .../androidtextinput/AndroidTextInputState.h | 38 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index cad08f0f9e8..82661d79383 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -81,9 +81,12 @@ AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString( auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.apply(getProps()->textAttributes); + // If there's no text, it's possible that this Fragment isn't actually + // appended to the AttributedString (see implementation of appendFragment) fragment.textAttributes = textAttributes; fragment.parentShadowView = ShadowView(*this); textAttributedString.appendFragment(fragment); + return textAttributedString; } @@ -112,10 +115,20 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { return; } + // Store default TextAttributes in state. + // In the case where the TextInput is completely empty (no value, no + // defaultValue, no placeholder, no children) there are therefore no fragments + // in the AttributedString, and when State is updated, it needs some way to + // reconstruct a Fragment with default TextAttributes. + auto defaultTextAttributes = TextAttributes::defaultTextAttributes(); + defaultTextAttributes.apply(getProps()->textAttributes); + setStateData(AndroidTextInputState{state.mostRecentEventCount, reactTreeAttributedString, reactTreeAttributedString, getProps()->paragraphAttributes, + defaultTextAttributes, + ShadowView(*this), textLayoutManager_}); } diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h index b305fefb654..e00cb66aa2f 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h @@ -46,6 +46,20 @@ class AndroidTextInputState final { */ ParagraphAttributes paragraphAttributes{}; + /** + * Default TextAttributes used if we need to construct a new Fragment. + * Only used if text is inserted into an AttributedString with no existing + * Fragments. + */ + TextAttributes defaultTextAttributes; + + /** + * Default parent ShadowView used if we need to construct a new Fragment. + * Only used if text is inserted into an AttributedString with no existing + * Fragments. + */ + ShadowView defaultParentShadowView; + /* * `TextLayoutManager` provides a connection to platform-specific * text rendering infrastructure which is capable to render the @@ -55,6 +69,8 @@ class AndroidTextInputState final { #ifdef ANDROID AttributedString updateAttributedString( + TextAttributes const &defaultTextAttributes, + ShadowView const &defaultParentShadowView, AttributedString const &original, folly::dynamic const &data) { if (data["textChanged"].empty()) { @@ -82,6 +98,15 @@ class AndroidTextInputState final { i++; } + if (fragments.size() > original.getFragments().size()) { + for (; i < fragments.size(); i++) { + str.appendFragment( + AttributedString::Fragment{fragments[i]["string"].getString(), + defaultTextAttributes, + defaultParentShadowView}); + } + } + return str; } @@ -90,21 +115,30 @@ class AndroidTextInputState final { AttributedString const &attributedString, AttributedString const &reactTreeAttributedString, ParagraphAttributes const ¶graphAttributes, + TextAttributes const &defaultTextAttributes, + ShadowView const &defaultParentShadowView, SharedTextLayoutManager const &layoutManager) : mostRecentEventCount(mostRecentEventCount), attributedString(attributedString), reactTreeAttributedString(reactTreeAttributedString), paragraphAttributes(paragraphAttributes), + defaultTextAttributes(defaultTextAttributes), + defaultParentShadowView(defaultParentShadowView), layoutManager(layoutManager) {} AndroidTextInputState() = default; AndroidTextInputState( AndroidTextInputState const &previousState, folly::dynamic const &data) : mostRecentEventCount((int64_t)data["mostRecentEventCount"].getInt()), - attributedString( - updateAttributedString(previousState.attributedString, data)), + attributedString(updateAttributedString( + previousState.defaultTextAttributes, + previousState.defaultParentShadowView, + previousState.attributedString, + data)), reactTreeAttributedString(previousState.reactTreeAttributedString), paragraphAttributes(previousState.paragraphAttributes), + defaultTextAttributes(previousState.defaultTextAttributes), + defaultParentShadowView(previousState.defaultParentShadowView), layoutManager(previousState.layoutManager){}; folly::dynamic getDynamic() const; #endif