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

[HOLD facebook/react-native#37465] Fix TextInput vertical alignment issue when using lineHeight prop on iOS (Paper - old arch) #57

14 changes: 11 additions & 3 deletions packages/react-native/Libraries/Text/RCTTextAttributes.m
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,12 @@ - (NSParagraphStyle *)effectiveParagraphStyle

if (!isnan(_lineHeight)) {
CGFloat lineHeight = _lineHeight * self.effectiveFontSizeMultiplier;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
isParagraphStyleUsed = YES;
// text with lineHeight lower then font.lineHeight does not correctly vertically align
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you capitalize all comments and add a line break before them (except for the one after the if which is in a new level of indentation)?

Suggested change
// text with lineHeight lower then font.lineHeight does not correctly vertically align
// Text with lineHeight lower then font.lineHeight does not correctly vertically align

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cead22. I made the changes with commit 12b51dd. I remain available for further improvements.

if (lineHeight > self.effectiveFont.lineHeight) {
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
isParagraphStyleUsed = YES;
}
}

if (isParagraphStyleUsed) {
Expand Down Expand Up @@ -172,6 +175,11 @@ - (NSParagraphStyle *)effectiveParagraphStyle
NSParagraphStyle *paragraphStyle = [self effectiveParagraphStyle];
if (paragraphStyle) {
attributes[NSParagraphStyleAttributeName] = paragraphStyle;
// The baseline aligns the text vertically in the line height
if (!isnan(paragraphStyle.maximumLineHeight) && paragraphStyle.maximumLineHeight >= font.lineHeight) {
CGFloat baseLineOffset = (paragraphStyle.maximumLineHeight - font.lineHeight) / 2.0;
attributes[NSBaselineOffsetAttributeName] = @(baseLineOffset);
}
}

// Decoration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@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,14 @@ - (void)paste:(id)sender
[super paste:sender];
}

- (void)setTextBorderInsetsAndFrame:(CGRect)bounds textBorderInsets:(UIEdgeInsets)textBorderInsets
{
_textBorderInsets = textBorderInsets;
// We apply `borderInsets` as `RCTUITextView` layout offset.
self.frame = UIEdgeInsetsInsetRect(bounds, textBorderInsets);
[self setNeedsLayout];
}

// Turn off scroll animation to fix flaky scrolling.
// This is only necessary for iOS <= 13.
#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 @@ -34,6 +34,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 @@ -42,6 +44,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,24 @@ - (void)uiManagerWillPerformMounting

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

// Fixes iOS issue caused by adding paragraphStyle.maximumLineHeight to an iOS UITextField.
// The CALayer _UITextLayoutFragmentView does not align correctly (see issue #28012).
if (!isnan(textAttributes.lineHeight) && !isnan(textAttributes.effectiveFont.lineHeight)) {
CGFloat effectiveLineHeight = textAttributes.lineHeight * textAttributes.effectiveFontSizeMultiplier;
CGFloat fontLineHeight = textAttributes.effectiveFont.lineHeight;
if (effectiveLineHeight >= fontLineHeight * 2.0) {
CGFloat height = self.layoutMetrics.frame.size.height;
CGFloat width = self.layoutMetrics.frame.size.width;
// sets the same origin.y coordinates for _UITextLayoutFragmentView and UITextField frame
baseTextInputView.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
// vertically center aligns the _UITextLayoutFragmentView in the parent UITextField
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 @@ -76,6 +76,22 @@ - (void)enforceTextAttributesIfNeeded
backedTextInputView.defaultTextAttributes = textAttributes;
}

// Fixes iOS alignment issue caused by adding paragraphStyle.maximumLineHeight to an iOS UITextField
// 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 +103,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 @@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@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 @@ -157,7 +163,17 @@ - (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