diff --git a/packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm b/packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm index a02622d5d7d7..8152757f4a56 100644 --- a/packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm +++ b/packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm @@ -14,6 +14,7 @@ #import #include +#include #include #include @@ -245,6 +246,76 @@ - (void)testSamenessOfMissingInsensitiveAttributes attributedString2, attributedString1, insensitiveAttributes, TextAttributes{})); } +- (void)testSamenessOfDifferentEventEmitters +{ + // The emitter is metadata a TextInput never dispatches through, so even a different one is not a difference. + auto eventEmitter1 = std::make_shared(nullptr, EventDispatcher::Weak{}); + auto eventEmitter2 = std::make_shared(nullptr, EventDispatcher::Weak{}); + + NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString1 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 5)]; + [attributedString1 addAttribute:RCTAttributedStringEventEmitterKey + value:RCTWrapEventEmitter(eventEmitter1) + range:NSMakeRange(0, 12)]; + + NSMutableAttributedString *attributedString2 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString2 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 5)]; + [attributedString2 addAttribute:RCTAttributedStringEventEmitterKey + value:RCTWrapEventEmitter(eventEmitter2) + range:NSMakeRange(0, 12)]; + + NSDictionary *insensitiveAttributes = @{}; + + XCTAssertTrue(RCTIsAttributedStringEffectivelySame( + attributedString1, attributedString2, insensitiveAttributes, TextAttributes{})); + XCTAssertTrue(RCTIsAttributedStringEffectivelySame( + attributedString2, attributedString1, insensitiveAttributes, TextAttributes{})); +} + +- (void)testSamenessOfMissingEventEmitter +{ + // Text typed into a field carries no event emitter; the state echoing it does. + auto eventEmitter = std::make_shared(nullptr, EventDispatcher::Weak{}); + + NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString1 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 5)]; + [attributedString1 addAttribute:RCTAttributedStringEventEmitterKey + value:RCTWrapEventEmitter(eventEmitter) + range:NSMakeRange(0, 12)]; + + NSMutableAttributedString *attributedString2 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString2 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 5)]; + + NSDictionary *insensitiveAttributes = @{}; + + XCTAssertTrue(RCTIsAttributedStringEffectivelySame( + attributedString1, attributedString2, insensitiveAttributes, TextAttributes{})); + XCTAssertTrue(RCTIsAttributedStringEffectivelySame( + attributedString2, attributedString1, insensitiveAttributes, TextAttributes{})); +} + +- (void)testDifferenceOfOtherAttributesNextToEventEmitter +{ + // Ignoring the event emitter must not hide a real difference in the same run. + auto eventEmitter = std::make_shared(nullptr, EventDispatcher::Weak{}); + + NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString1 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 12)]; + [attributedString1 addAttribute:RCTAttributedStringEventEmitterKey + value:RCTWrapEventEmitter(eventEmitter) + range:NSMakeRange(0, 12)]; + + NSMutableAttributedString *attributedString2 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; + [attributedString2 addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, 12)]; + + NSDictionary *insensitiveAttributes = @{}; + + XCTAssertFalse(RCTIsAttributedStringEffectivelySame( + attributedString1, attributedString2, insensitiveAttributes, TextAttributes{})); + XCTAssertFalse(RCTIsAttributedStringEffectivelySame( + attributedString2, attributedString1, insensitiveAttributes, TextAttributes{})); +} + - (void)testSamenessOfResolvedParagraphStyleLtr { NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"Hello world!"]; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 791d8d129473..16b942dadfe9 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -568,6 +568,13 @@ static BOOL RCTIsAttributeEffectivelySame( NSDictionary *insensitiveAttributes, const TextAttributes &baseTextAttributes) { + // The event emitter is React-internal metadata, not something UIKit shows, and a TextInput dispatches its events + // through its own emitter rather than through this attribute. Text typed into the field often carries no + // emitter while the state echoing it does, so comparing it reports a difference for identical text. + if ([attributeKey isEqualToString:RCTAttributedStringEventEmitterKey]) { + return YES; + } + id attribute1 = attributes1[attributeKey] ?: insensitiveAttributes[attributeKey]; id attribute2 = attributes2[attributeKey] ?: insensitiveAttributes[attributeKey];