Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,21 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
if (shouldFallbackToBareTextComparison) {
return [newText.string isEqualToString:oldText.string];
} else {
// Typed text carries attributes a state built from props (a controlled `value`) does not: the input's
// `defaultTextAttributes` (the first character typed into an empty field also gets their background color and
// event emitter) and a paragraph style, which is UIKit's default one or the one in its initial typing attributes
// (another line break strategy). Both are accepted.
const auto &props = static_cast<const TextInputProps &>(*_props);
NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
[_originalTypingAttributes mutableCopy] ?: [NSMutableDictionary new];
[defaultAttributes addEntriesFromDictionary:_backedTextInputView.defaultTextAttributes];
defaultAttributes[NSParagraphStyleAttributeName] =
_backedTextInputView.defaultTextAttributes[NSParagraphStyleAttributeName]
?: NSParagraphStyle.defaultParagraphStyle;
return RCTIsAttributedStringEffectivelySame(
newText, oldText, _originalTypingAttributes, static_cast<const TextInputProps &>(*_props).textAttributes);
newText, oldText, defaultAttributes, props.textAttributes, !props.multiline) ||
RCTIsAttributedStringEffectivelySame(
newText, oldText, _originalTypingAttributes, props.textAttributes, !props.multiline);
}
}

Expand Down
172 changes: 172 additions & 0 deletions packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,178 @@ - (void)testDifferenceOfMissingParagraphStyle
attributedString2, attributedString1, insensitiveAttributes, textAttributes));
}

#pragma mark - RCTIsAttributedStringEffectivelySame for the text a text input shows

static NSDictionary<NSAttributedStringKey, id> *RCTTestTextAttributes(void)
{
return @{
NSFontAttributeName : [UIFont systemFontOfSize:14],
NSForegroundColorAttributeName : UIColor.blackColor,
};
}

// The insensitive attributes of a text input: its default text attributes and UIKit's default paragraph style.
static NSDictionary<NSAttributedStringKey, id> *RCTTestInsensitiveAttributes(void)
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [RCTTestTextAttributes() mutableCopy];
attributes[NSBackgroundColorAttributeName] = UIColor.clearColor;
attributes[NSParagraphStyleAttributeName] = NSParagraphStyle.defaultParagraphStyle;
return attributes;
}

// What UIKit gives text typed into a text field whose text attributes set no paragraph style.
static NSParagraphStyle *RCTTestTypedParagraphStyle(void)
{
NSMutableParagraphStyle *style = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
style.lineBreakMode = NSLineBreakByTruncatingTail;
style.baseWritingDirection = NSWritingDirectionLeftToRight;
return style;
}

static NSAttributedString *RCTTestString(NSString *string, NSParagraphStyle *_Nullable style)
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [RCTTestTextAttributes() mutableCopy];
attributes[NSParagraphStyleAttributeName] = style;
return [[NSAttributedString alloc] initWithString:string attributes:attributes];
}

// Whether a text input showing `shown` already shows `state`, both ways round.
static BOOL
RCTTestShowsBothWays(XCTestCase *self, NSAttributedString *shown, NSAttributedString *state, BOOL singleLine)
{
BOOL same =
RCTIsAttributedStringEffectivelySame(state, shown, RCTTestInsensitiveAttributes(), TextAttributes{}, singleLine);
BOOL reversed =
RCTIsAttributedStringEffectivelySame(shown, state, RCTTestInsensitiveAttributes(), TextAttributes{}, singleLine);
XCTAssertEqual(same, reversed);
return same;
}

#define RCTTestShows(shown, state, singleLine) RCTTestShowsBothWays(self, shown, state, singleLine)

- (void)testSamenessOfTypedTextWithUIKitsDefaultParagraphStyle
{
// The state built from a controlled `value` has no paragraph style; the typed text has UIKit's default one.
NSAttributedString *state = RCTTestString(@"abc", nil);
NSAttributedString *shown = RCTTestString(@"abc", RCTTestTypedParagraphStyle());

XCTAssertTrue(RCTTestShows(shown, state, YES));
// A text view does not set its own line break mode: another one in a multiline input is a change.
XCTAssertFalse(RCTTestShows(shown, state, NO));
}

- (void)testSamenessOfTypedTextWithTheInputsDefaultAttributes
{
// The first character typed into an empty field gets the field's default attributes after it reached JS.
NSMutableAttributedString *shown = [RCTTestString(@"/", RCTTestTypedParagraphStyle()) mutableCopy];
[shown addAttribute:NSBackgroundColorAttributeName value:UIColor.clearColor range:NSMakeRange(0, 1)];

XCTAssertTrue(RCTTestShows(shown, RCTTestString(@"/", nil), YES));
}

- (void)testSamenessOfAnyLineBreakModeInASingleLineField
{
NSMutableParagraphStyle *wrapping = [NSMutableParagraphStyle new];
wrapping.lineBreakMode = NSLineBreakByWordWrapping;
NSMutableParagraphStyle *truncating = [NSMutableParagraphStyle new];
truncating.lineBreakMode = NSLineBreakByTruncatingTail;

XCTAssertTrue(RCTTestShows(RCTTestString(@"abc", truncating), RCTTestString(@"abc", wrapping), YES));
// A multiline input shows the line break mode JS sets (`lineBreakModeIOS`).
XCTAssertFalse(RCTTestShows(RCTTestString(@"abc", truncating), RCTTestString(@"abc", wrapping), NO));
}

- (void)testSamenessOfASingleLineFieldWithoutTheLineHeight
{
// `lineHeight` on a single-line input: the text field shows the text without the line height, with the writing
// direction resolved.
NSMutableParagraphStyle *stateStyle = [NSMutableParagraphStyle new];
stateStyle.minimumLineHeight = 26;
stateStyle.maximumLineHeight = 26;
NSMutableParagraphStyle *shownStyle = [RCTTestTypedParagraphStyle() mutableCopy];

XCTAssertTrue(RCTTestShows(RCTTestString(@"p", shownStyle), RCTTestString(@"p", stateStyle), YES));
// A multiline input keeps the line height.
XCTAssertFalse(RCTTestShows(RCTTestString(@"p", shownStyle), RCTTestString(@"p", stateStyle), NO));
}

- (void)testDifferenceOfAnotherLineHeightInAMultilineInput
{
NSMutableParagraphStyle *stateStyle = [NSMutableParagraphStyle new];
stateStyle.minimumLineHeight = 30;
stateStyle.maximumLineHeight = 30;
NSMutableParagraphStyle *shownStyle = [stateStyle mutableCopy];
shownStyle.minimumLineHeight = 26;
shownStyle.maximumLineHeight = 26;

XCTAssertFalse(RCTTestShows(RCTTestString(@"abc", shownStyle), RCTTestString(@"abc", stateStyle), NO));
}

- (void)testDifferenceOfAnotherAlignment
{
NSMutableParagraphStyle *centered = [NSMutableParagraphStyle new];
centered.alignment = NSTextAlignmentCenter;

XCTAssertFalse(
RCTTestShows(RCTTestString(@"abc", RCTTestTypedParagraphStyle()), RCTTestString(@"abc", centered), YES));
// Shown centered while the state sets no paragraph style: a change too.
XCTAssertFalse(RCTTestShows(RCTTestString(@"abc", centered), RCTTestString(@"abc", nil), YES));
}

- (void)testDifferenceOfAChangedOrRemovedAttribute
{
NSMutableAttributedString *restyled = [RCTTestString(@"abc", nil) mutableCopy];
[restyled addAttribute:NSForegroundColorAttributeName value:UIColor.redColor range:NSMakeRange(0, 3)];
XCTAssertFalse(RCTTestShows(RCTTestString(@"abc", RCTTestTypedParagraphStyle()), restyled, YES));

// JS removed the underline the field shows: not one of the input's default attributes.
NSMutableAttributedString *underlined = [RCTTestString(@"abc", nil) mutableCopy];
[underlined addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, 3)];
XCTAssertFalse(RCTTestShows(underlined, RCTTestString(@"abc", nil), YES));
}

- (void)testSamenessAndDifferenceOfLetterSpacing
{
NSMutableAttributedString *state = [RCTTestString(@"abc", nil) mutableCopy];
[state addAttribute:NSKernAttributeName value:@3 range:NSMakeRange(0, 3)];
NSMutableAttributedString *shown = [state mutableCopy];
[shown addAttribute:NSParagraphStyleAttributeName value:RCTTestTypedParagraphStyle() range:NSMakeRange(0, 3)];

XCTAssertTrue(RCTTestShows(shown, state, YES));
// JS removing the letter spacing is a change.
XCTAssertFalse(RCTTestShows(shown, RCTTestString(@"abc", nil), YES));
}

- (void)testSamenessOfRightToLeftTextInASingleLineField
{
// A text field shows the text with its own writing direction, whatever the state's paragraph style sets.
NSString *hebrew = @"\u05E9\u05DC\u05D5\u05DD";
NSMutableParagraphStyle *rtl = [NSMutableParagraphStyle new];
rtl.baseWritingDirection = NSWritingDirectionRightToLeft;
rtl.alignment = NSTextAlignmentRight;
NSMutableParagraphStyle *shown = [rtl mutableCopy];
shown.baseWritingDirection = NSWritingDirectionLeftToRight;
shown.lineBreakMode = NSLineBreakByTruncatingTail;

XCTAssertTrue(RCTTestShows(RCTTestString(hebrew, shown), RCTTestString(hebrew, rtl), YES));
// The alignment is still compared.
NSMutableParagraphStyle *centered = [rtl mutableCopy];
centered.alignment = NSTextAlignmentCenter;
XCTAssertFalse(RCTTestShows(RCTTestString(hebrew, shown), RCTTestString(hebrew, centered), YES));
// A multiline input keeps the direction.
XCTAssertFalse(RCTTestShows(RCTTestString(hebrew, shown), RCTTestString(hebrew, rtl), NO));
}

- (void)testSamenessOfEmojiAndCombiningMarks
{
NSString *emoji =
@"a\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466b\U0001F44D\U0001F3FDc\U0001F1EF\U0001F1F5de\u0301f";
XCTAssertTrue(RCTTestShows(RCTTestString(emoji, RCTTestTypedParagraphStyle()), RCTTestString(emoji, nil), YES));
NSString *precomposed = [emoji stringByReplacingOccurrencesOfString:@"e\u0301" withString:@"\u00E9"];
XCTAssertFalse(
RCTTestShows(RCTTestString(precomposed, RCTTestTypedParagraphStyle()), RCTTestString(emoji, nil), YES));
}

// A std::string may legitimately contain an embedded NUL. The C-string APIs
// these converters used to call stop at it, silently truncating user text.

Expand Down
Loading
Loading