Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TextInput vertical alignment issue when using lineHeight prop on iOS without changing Text baseline (Paper - old arch) #38258

Closed
Closed
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 @@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) BOOL dictationRecognizing;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) CGRect fragmentViewContainerBounds;
@property (nonatomic, assign) UIEdgeInsets textBorderInsets;
@property (nonatomic, assign) UIControlContentVerticalAlignment contentVerticalAlignment;

@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ - (void)paste:(id)sender
[super paste:sender];
}

- (void)setTextBorderInsetsAndFrame:(CGRect)bounds textBorderInsets:(UIEdgeInsets)textBorderInsets
{
_textBorderInsets = textBorderInsets;

// We apply `borderInsets` as the `RCTUITextView` layout offset.
self.frame = UIEdgeInsetsInsetRect(bounds, textBorderInsets);
[self setNeedsLayout];
}

// Turn off scroll animation to fix flaky scrolling.
// This is only necessary for iOS <= 14.
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 140000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) CGFloat zoomScale;
@property (nonatomic, assign, readonly) CGPoint contentOffset;
@property (nonatomic, assign, readonly) UIEdgeInsets contentInset;
@property (nonatomic, assign) CGRect fragmentViewContainerBounds;
@property (nonatomic, assign) UIControlContentVerticalAlignment contentVerticalAlignment;

// This protocol disallows direct access to `selectedTextRange` property because
// unwise usage of it can break the `delegate` behavior. So, we always have to
Expand All @@ -43,6 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
// If the change was a result of user actions (like typing or touches), we MUST notify the delegate.
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE;
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate;
- (void)setTextBorderInsetsAndFrame:(CGRect)bounds textBorderInsets:(UIEdgeInsets)textBorderInsets;

// This protocol disallows direct access to `text` property because
// unwise usage of it can break the `attributeText` behavior.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ - (void)uiManagerWillPerformMounting

baseTextInputView.textAttributes = textAttributes;
baseTextInputView.reactBorderInsets = borderInsets;

// The CALayer _UITextLayoutFragmentView does not align correctly
// when adding paragraphStyle.maximumLineHeight to an iOS UITextField (issue #28012).
CGFloat fontLineHeight = textAttributes.effectiveFont.lineHeight;
if (!isnan(textAttributes.lineHeight) && !isnan(fontLineHeight) && textAttributes.lineHeight > 0) {
CGFloat effectiveLineHeight = textAttributes.lineHeight * textAttributes.effectiveFontSizeMultiplier;
if (effectiveLineHeight >= fontLineHeight * 2.0) {
CGFloat height = self.layoutMetrics.frame.size.height;
CGFloat width = self.layoutMetrics.frame.size.width;

// Setting contentVerticalAlignment to UIControlContentVerticalAlignmentTop aligns
// _UITextLayoutFragmentView and UITextField on the same ordinate (y coordinate).
baseTextInputView.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;

// Align vertically _UITextLayoutFragmentView in the center of the UITextField (TextInput).
CGFloat padding = (height - effectiveLineHeight) / 2.0;
baseTextInputView.fragmentViewContainerBounds = CGRectMake(0, padding, width, effectiveLineHeight);
}
} else {
baseTextInputView.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
}
baseTextInputView.reactPaddingInsets = paddingInsets;

if (newAttributedText) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, nullable) RCTTextAttributes *textAttributes;
@property (nonatomic, assign) UIEdgeInsets reactPaddingInsets;
@property (nonatomic, assign) UIEdgeInsets reactBorderInsets;
@property (nonatomic, assign) CGRect fragmentViewContainerBounds;
@property (nonatomic, assign) UIControlContentVerticalAlignment contentVerticalAlignment;

@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onSelectionChange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,40 @@ - (void)enforceTextAttributesIfNeeded
[textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName];
}

UIFont *font = [textAttributes valueForKey:NSFontAttributeName];
NSParagraphStyle *paragraphStyle = [textAttributes valueForKey:NSParagraphStyleAttributeName];
if (!isnan(paragraphStyle.maximumLineHeight) && !isnan(font.lineHeight)) {
if (paragraphStyle.maximumLineHeight >= font.lineHeight) {
// The baseline aligns the text vertically in the line height (_UITextLayoutFragmentView).
CGFloat baseLineOffset = (paragraphStyle.maximumLineHeight - font.lineHeight) / 2.0;
[textAttributes setValue:@(baseLineOffset) forKey:NSBaselineOffsetAttributeName];
} else if (font.lineHeight > paragraphStyle.maximumLineHeight) {
// Text with lineHeight lower than font.lineHeight does not correctly vertically align.
NSMutableParagraphStyle *newParagraphStyle = [paragraphStyle mutableCopy];
newParagraphStyle.maximumLineHeight = font.lineHeight;
newParagraphStyle.minimumLineHeight = font.lineHeight;
[textAttributes setValue:newParagraphStyle forKey:NSParagraphStyleAttributeName];
}
}
backedTextInputView.defaultTextAttributes = textAttributes;
}

// Fixes iOS alignment issue caused by adding paragraphStyle.maximumLineHeight to an iOS UITextField
// and vertically aligns _UITextLayoutFragmentView with the parent view UITextField.
- (void)setContentVerticalAlignment:(UIControlContentVerticalAlignment)contentVerticalAlignment
{
_contentVerticalAlignment = contentVerticalAlignment;
self.backedTextInputView.contentVerticalAlignment = contentVerticalAlignment;
}

// Custom bounds used to control vertical position of CALayer _UITextLayoutFragmentView.
// _UITextLayoutFragmentView is the CALayer of UITextField.
- (void)setFragmentViewContainerBounds:(CGRect)fragmentViewContainerBounds
{
_fragmentViewContainerBounds = fragmentViewContainerBounds;
self.backedTextInputView.fragmentViewContainerBounds = fragmentViewContainerBounds;
}

- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
{
_reactPaddingInsets = reactPaddingInsets;
Expand All @@ -87,9 +118,8 @@ - (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
{
_reactBorderInsets = reactBorderInsets;
// We apply `borderInsets` as `backedTextInputView` layout offset.
self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
[self setNeedsLayout];
// Borders are added using insets (UITextField textRectForBound, UITextView setFrame).
[self.backedTextInputView setTextBorderInsetsAndFrame:self.bounds textBorderInsets:reactBorderInsets];
}

- (NSAttributedString *)attributedText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) BOOL dictationRecognizing;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, assign) CGRect fragmentViewContainerBounds;
@property (nonatomic, assign) UIEdgeInsets textBorderInsets;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
[self setNeedsLayout];
}

- (void)setTextBorderInsetsAndFrame:(CGRect)bounds textBorderInsets:(UIEdgeInsets)textBorderInsets
{
_textBorderInsets = textBorderInsets;
[self setNeedsLayout];
}

- (void)setPlaceholder:(NSString *)placeholder
{
[super setPlaceholder:placeholder];
Expand Down Expand Up @@ -170,7 +176,18 @@ - (CGRect)caretRectForPosition:(UITextPosition *)position

- (CGRect)textRectForBounds:(CGRect)bounds
{
return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], _textContainerInset);
// Text is vertically aligned to the center.
CGFloat leftPadding = _textContainerInset.left + _textBorderInsets.left;
CGFloat rightPadding = _textContainerInset.right + _textBorderInsets.right;
UIEdgeInsets borderAndPaddingInsets =
UIEdgeInsetsMake(_textContainerInset.top, leftPadding, _textContainerInset.bottom, rightPadding);
if (self.fragmentViewContainerBounds.size.height > 0) {
// apply custom bounds to fix iOS UITextField issue with lineHeight
// sets the correct y coordinates for _UITextLayoutFragmentView
return UIEdgeInsetsInsetRect([super textRectForBounds:self.fragmentViewContainerBounds], borderAndPaddingInsets);
} else {
return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], borderAndPaddingInsets);
}
}

- (CGRect)editingRectForBounds:(CGRect)bounds
Expand Down