Files
react-native/ReactCommon/react/renderer/attributedstring/AttributedString.cpp
T
Nick Gerleman 089c9a5c9c Fix AttributedString comparison logic for TextInput state updates
Summary:
D37801394 (https://github.com/facebook/react-native/commit/51f49ca9982f24de08f5a5654a5210e547bb5b86) attempted to fix an issue of TextInput values being dropped when an uncontrolled component is restyled, and a defaultValue is present. I had missed quite a bit of functionality, where TextInput may have child Text elements, which the native side flattens into a single AttributedString. `lastNativeValue` includes a lossy version of the flattened string  produced from the child fragments, so sending it along with the children led to duplicating of the current input on each edit, and things blow up.

With some experimentation, I found that the text-loss behavior only happens on Fabric, and is triggered by a state update rather than my original assumption of the view manager command in the `useLayoutEffect` hook. `AndroidTextInputShadowNode` will compare the current and previous flattened strings, to intentionally allow the native value to drift from the React tree if the React tree hasn't changed. This `AttributedString` comparison includes layout metrics as of D20151505 (https://github.com/facebook/react-native/commit/061f54e89086af1c80e5b0460ec715533f99bdb7) meaning a restyle may cause a state update, and clear the text.

I do not have full understanding of the flow of state updates to layout, or the underlying issue that led to the equality check including layout information (since TextMeasurementCache seems to explicitly compare LayoutMetrics). D18894538 (https://github.com/facebook/react-native/commit/254ebab1d2b6fac859ab1ae0c9503328fc99a6d0) used a solution of sending a no-op state update to avoid updating text for placeholders, when the Attributed strings are equal (though as of now this code is never reached, since we return earlier on AttributedString equality). I co-opted this mechanism, to avoid sending text updates if the text content and attributes of the AttributedString has not changed, disregarding any layout information. This is how the comparison worked at the time of the diff.

I also updated the fragment hashing function to include layout metrics, since it was added to be part of the equality check, and is itself hashable.

Changelog:
[Android][Fixed] - Fix `AttributedString` comparison logic for TextInput state updates

Reviewed By: sammy-SC

Differential Revision: D37902643

fbshipit-source-id: c0f8e3112feb19bd0ee62b37bdadeb237a9f725e
2022-07-18 18:20:22 -07:00

171 lines
4.0 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AttributedString.h"
#include <react/renderer/debug/DebugStringConvertibleItem.h>
namespace facebook {
namespace react {
using Fragment = AttributedString::Fragment;
using Fragments = AttributedString::Fragments;
#pragma mark - Fragment
std::string Fragment::AttachmentCharacter() {
return u8"\uFFFC"; // Unicode `OBJECT REPLACEMENT CHARACTER`
}
bool Fragment::isAttachment() const {
return string == AttachmentCharacter();
}
bool Fragment::operator==(const Fragment &rhs) const {
return std::tie(
string,
textAttributes,
parentShadowView.tag,
parentShadowView.layoutMetrics) ==
std::tie(
rhs.string,
rhs.textAttributes,
rhs.parentShadowView.tag,
rhs.parentShadowView.layoutMetrics);
}
bool Fragment::isContentEqual(const Fragment &rhs) const {
return std::tie(string, textAttributes) ==
std::tie(rhs.string, rhs.textAttributes);
}
bool Fragment::operator!=(const Fragment &rhs) const {
return !(*this == rhs);
}
#pragma mark - AttributedString
void AttributedString::appendFragment(const Fragment &fragment) {
ensureUnsealed();
if (fragment.string.empty()) {
return;
}
fragments_.push_back(fragment);
}
void AttributedString::prependFragment(const Fragment &fragment) {
ensureUnsealed();
if (fragment.string.empty()) {
return;
}
fragments_.insert(fragments_.begin(), fragment);
}
void AttributedString::appendAttributedString(
const AttributedString &attributedString) {
ensureUnsealed();
fragments_.insert(
fragments_.end(),
attributedString.fragments_.begin(),
attributedString.fragments_.end());
}
void AttributedString::prependAttributedString(
const AttributedString &attributedString) {
ensureUnsealed();
fragments_.insert(
fragments_.begin(),
attributedString.fragments_.begin(),
attributedString.fragments_.end());
}
Fragments const &AttributedString::getFragments() const {
return fragments_;
}
Fragments &AttributedString::getFragments() {
return fragments_;
}
std::string AttributedString::getString() const {
auto string = std::string{};
for (const auto &fragment : fragments_) {
string += fragment.string;
}
return string;
}
bool AttributedString::isEmpty() const {
return fragments_.empty();
}
bool AttributedString::compareTextAttributesWithoutFrame(
const AttributedString &rhs) const {
if (fragments_.size() != rhs.fragments_.size()) {
return false;
}
for (unsigned i = 0; i < fragments_.size(); i++) {
if (fragments_[i].textAttributes != rhs.fragments_[i].textAttributes ||
fragments_[i].string != rhs.fragments_[i].string) {
return false;
}
}
return true;
}
bool AttributedString::operator==(const AttributedString &rhs) const {
return fragments_ == rhs.fragments_;
}
bool AttributedString::operator!=(const AttributedString &rhs) const {
return !(*this == rhs);
}
bool AttributedString::isContentEqual(const AttributedString &rhs) const {
if (fragments_.size() != rhs.fragments_.size()) {
return false;
}
for (auto i = 0; i < fragments_.size(); i++) {
if (!fragments_[i].isContentEqual(rhs.fragments_[i])) {
return false;
}
}
return true;
}
#pragma mark - DebugStringConvertible
#if RN_DEBUG_STRING_CONVERTIBLE
SharedDebugStringConvertibleList AttributedString::getDebugChildren() const {
auto list = SharedDebugStringConvertibleList{};
for (auto &&fragment : fragments_) {
auto propsList =
fragment.textAttributes.DebugStringConvertible::getDebugProps();
list.push_back(std::make_shared<DebugStringConvertibleItem>(
"Fragment",
fragment.string,
SharedDebugStringConvertibleList(),
propsList));
}
return list;
}
#endif
} // namespace react
} // namespace facebook