Skip to content

Commit 1b013cd

Browse files
sherginfacebook-github-bot
authored andcommitted
Better TextInput: Fixing multiline <TextInput> insets and prepare for auto-expanding feature
Summary: Several things: * The mess with insets was fixed. Previously we tried to compensate the insets difference with `UITextField` by adjusting `textContainerInset` property, moreover we delegated negative part of this compensation to the view inset. That was terrible because it breaks `contentSize` computation, complicates whole insets consept, complicates everything; it just was not right. Now we are fixing the top and left inset differences in different places. We disable left and right 5pt margin by setting `_textView.textContainer.lineFragmentPadding = 0` and we introduce top 5px inset as a DEFAULT value for top inset for common multiline <TextInput> (this value can be easilly overwritten in Javascript). * Internal layout and contentSize computations were unified and simplified. * Now we report `intrinsicContentSize` value to Yoga, one step before auto-expandable TextInput. Depends on D4640207. Reviewed By: mmmulani Differential Revision: D4645921 fbshipit-source-id: da5988ebac50be967caecd71e780c014f6eb257a
1 parent 3acafd1 commit 1b013cd

File tree

5 files changed

+88
-65
lines changed

5 files changed

+88
-65
lines changed

Libraries/Components/TextInput/TextInput.js

+7
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ const TextInput = React.createClass({
678678
if (props.inputView) {
679679
children = [children, props.inputView];
680680
}
681+
props.style.unshift(styles.multilineInput);
681682
textContainer =
682683
<RCTTextView
683684
ref={this._setNativeRef}
@@ -867,6 +868,12 @@ var styles = StyleSheet.create({
867868
input: {
868869
alignSelf: 'stretch',
869870
},
871+
multilineInput: {
872+
// This default top inset makes RCTTextView seem as close as possible
873+
// to single-line RCTTextField defaults, using the system defaults
874+
// of font size 17 and a height of 31 points.
875+
paddingTop: 5,
876+
},
870877
});
871878

872879
module.exports = TextInput;

Libraries/Text/RCTTextView.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#import <React/RCTView.h>
1313
#import <React/UIView+React.h>
1414

15-
@class RCTEventDispatcher;
15+
@class RCTBridge;
1616

1717
@interface RCTTextView : RCTView <UITextViewDelegate>
1818

@@ -28,14 +28,15 @@
2828
@property (nonatomic, strong) UIFont *font;
2929
@property (nonatomic, assign) NSInteger mostRecentEventCount;
3030
@property (nonatomic, strong) NSNumber *maxLength;
31+
@property (nonatomic, assign, readonly) CGSize contentSize;
3132

3233
@property (nonatomic, copy) RCTDirectEventBlock onChange;
3334
@property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange;
3435
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
3536
@property (nonatomic, copy) RCTDirectEventBlock onTextInput;
3637
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
3738

38-
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
39+
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
3940

4041
- (void)performTextUpdate;
4142

Libraries/Text/RCTTextView.m

+75-61
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#import <React/RCTConvert.h>
1313
#import <React/RCTEventDispatcher.h>
14+
#import <React/RCTUIManager.h>
1415
#import <React/RCTUtils.h>
1516
#import <React/UIView+React.h>
1617

@@ -69,6 +70,7 @@ - (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated
6970

7071
@implementation RCTTextView
7172
{
73+
RCTBridge *_bridge;
7274
RCTEventDispatcher *_eventDispatcher;
7375

7476
NSString *_placeholder;
@@ -87,22 +89,25 @@ @implementation RCTTextView
8789
NSInteger _nativeEventCount;
8890

8991
CGSize _previousContentSize;
90-
BOOL _viewDidCompleteInitialLayout;
9192
}
9293

93-
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
94+
- (instancetype)initWithBridge:(RCTBridge *)bridge
9495
{
95-
RCTAssertParam(eventDispatcher);
96+
RCTAssertParam(bridge);
9697

97-
if ((self = [super initWithFrame:CGRectZero])) {
98+
if (self = [super initWithFrame:CGRectZero]) {
9899
_contentInset = UIEdgeInsetsZero;
99-
_eventDispatcher = eventDispatcher;
100+
_bridge = bridge;
101+
_eventDispatcher = bridge.eventDispatcher;
100102
_placeholderTextColor = [self defaultPlaceholderTextColor];
101103
_blurOnSubmit = NO;
102104

103-
_textView = [[RCTUITextView alloc] initWithFrame:CGRectZero];
105+
_textView = [[RCTUITextView alloc] initWithFrame:self.bounds];
106+
_textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
104107
_textView.backgroundColor = [UIColor clearColor];
105108
_textView.textColor = [UIColor blackColor];
109+
// This line actually removes 5pt (default value) left and right padding in UITextView.
110+
_textView.textContainer.lineFragmentPadding = 0;
106111
#if !TARGET_OS_TV
107112
_textView.scrollsToTop = NO;
108113
#endif
@@ -132,7 +137,7 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
132137
// If this <TextInput> is in rich text editing mode, and the child <Text> node providing rich text
133138
// styling has a backgroundColor, then the attributedText produced by the child <Text> node will have an
134139
// NSBackgroundColor attribute. We need to forward this attribute to the text view manually because the text view
135-
// always has a clear background color in -initWithEventDispatcher:.
140+
// always has a clear background color in `initWithBridge:`.
136141
//
137142
// TODO: This should be removed when the related hack in -performPendingTextUpdate is removed.
138143
if (subview.backgroundColor) {
@@ -237,60 +242,20 @@ - (void)performPendingTextUpdate
237242
[_textView layoutIfNeeded];
238243

239244
[self updatePlaceholderVisibility];
240-
[self updateContentSize];
245+
[self invalidateContentSize];
241246

242247
_blockTextShouldChange = NO;
243248
}
244249

245-
- (void)updateFrames
246-
{
247-
// Adjust the insets so that they are as close as possible to single-line
248-
// RCTTextField defaults, using the system defaults of font size 17 and a
249-
// height of 31 points.
250-
//
251-
// We apply the left inset to the frame since a negative left text-container
252-
// inset mysteriously causes the text to be hidden until the text view is
253-
// first focused.
254-
UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero;
255-
adjustedFrameInset.left = _contentInset.left - 5;
256-
257-
UIEdgeInsets adjustedTextContainerInset = _contentInset;
258-
adjustedTextContainerInset.top += 5;
259-
adjustedTextContainerInset.left = 0;
260-
261-
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset);
262-
_textView.frame = frame;
263-
_placeholderView.frame = frame;
264-
[self updateContentSize];
265-
266-
_textView.textContainerInset = adjustedTextContainerInset;
267-
_placeholderView.textContainerInset = adjustedTextContainerInset;
268-
}
269-
270-
- (void)updateContentSize
271-
{
272-
CGSize size = _textView.frame.size;
273-
size.height = [_textView sizeThatFits:CGSizeMake(size.width, INFINITY)].height;
274-
275-
if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) {
276-
_previousContentSize = size;
277-
_onContentSizeChange(@{
278-
@"contentSize": @{
279-
@"height": @(size.height),
280-
@"width": @(size.width),
281-
},
282-
@"target": self.reactTag,
283-
});
284-
}
285-
}
286-
287250
- (void)updatePlaceholder
288251
{
289252
[_placeholderView removeFromSuperview];
290253
_placeholderView = nil;
291254

292255
if (_placeholder) {
293-
_placeholderView = [[UITextView alloc] initWithFrame:self.bounds];
256+
_placeholderView = [[UITextView alloc] initWithFrame:_textView.frame];
257+
_placeholderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
258+
_placeholderView.textContainer.lineFragmentPadding = 0;
294259
_placeholderView.userInteractionEnabled = NO;
295260
_placeholderView.backgroundColor = [UIColor clearColor];
296261
_placeholderView.scrollEnabled = NO;
@@ -340,7 +305,9 @@ - (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
340305
- (void)setContentInset:(UIEdgeInsets)contentInset
341306
{
342307
_contentInset = contentInset;
343-
[self updateFrames];
308+
_textView.textContainerInset = contentInset;
309+
_placeholderView.textContainerInset = contentInset;
310+
[self setNeedsLayout];
344311
}
345312

346313
#pragma mark - UITextViewDelegate
@@ -503,8 +470,7 @@ - (void)setText:(NSString *)text
503470
}
504471

505472
[self updatePlaceholderVisibility];
506-
[self updateContentSize]; //keep the text wrapping when the length of
507-
//the textline has been extended longer than the length of textinputView
473+
[self invalidateContentSize];
508474
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
509475
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
510476
}
@@ -595,7 +561,7 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
595561
- (void)textViewDidChange:(UITextView *)textView
596562
{
597563
[self updatePlaceholderVisibility];
598-
[self updateContentSize];
564+
[self invalidateContentSize];
599565

600566
// Detect when textView updates happend that didn't invoke `shouldChangeTextInRange`
601567
// (e.g. typing simplified chinese in pinyin will insert and remove spaces without
@@ -664,6 +630,8 @@ - (void)textViewDidEndEditing:(UITextView *)textView
664630
eventCount:_nativeEventCount];
665631
}
666632

633+
#pragma mark - UIResponder
634+
667635
- (BOOL)isFirstResponder
668636
{
669637
return [_textView isFirstResponder];
@@ -695,17 +663,63 @@ - (BOOL)resignFirstResponder
695663
return [_textView resignFirstResponder];
696664
}
697665

698-
- (void)layoutSubviews
666+
#pragma mark - Content Size
667+
668+
- (CGSize)contentSize
699669
{
700-
[super layoutSubviews];
670+
// Returning value does NOT include insets.
671+
CGSize contentSize = self.intrinsicContentSize;
672+
contentSize.width -= _contentInset.left + _contentInset.right;
673+
contentSize.height -= _contentInset.top + _contentInset.bottom;
674+
return contentSize;
675+
}
676+
677+
- (void)invalidateContentSize
678+
{
679+
CGSize contentSize = self.contentSize;
680+
681+
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
682+
return;
683+
}
684+
_previousContentSize = contentSize;
685+
686+
[_bridge.uiManager setIntrinsicContentSize:contentSize forView:self];
687+
688+
if (_onContentSizeChange) {
689+
_onContentSizeChange(@{
690+
@"contentSize": @{
691+
@"height": @(contentSize.height),
692+
@"width": @(contentSize.width),
693+
},
694+
@"target": self.reactTag,
695+
});
696+
}
697+
}
698+
699+
#pragma mark - Layout
700+
701+
- (CGSize)intrinsicContentSize
702+
{
703+
// Calling `sizeThatFits:` is probably more expensive method to compute
704+
// content size compare to direct access `_textView.contentSize` property,
705+
// but seems `sizeThatFits:` returns more reliable and consistent result.
706+
// Returning value DOES include insets.
707+
return [self sizeThatFits:CGSizeMake(self.bounds.size.width, INFINITY)];
708+
}
701709

702-
// Start sending content size updates only after the view has been laid out
703-
// otherwise we send multiple events with bad dimensions on initial render.
704-
_viewDidCompleteInitialLayout = YES;
710+
- (CGSize)sizeThatFits:(CGSize)size
711+
{
712+
return [_textView sizeThatFits:size];
713+
}
705714

706-
[self updateFrames];
715+
- (void)layoutSubviews
716+
{
717+
[super layoutSubviews];
718+
[self invalidateContentSize];
707719
}
708720

721+
#pragma mark - Default values
722+
709723
- (UIFont *)defaultPlaceholderFont
710724
{
711725
return [UIFont systemFontOfSize:17];

Libraries/Text/RCTTextViewManager.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ - (RCTShadowView *)shadowView
2929

3030
- (UIView *)view
3131
{
32-
return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
32+
return [[RCTTextView alloc] initWithBridge:self.bridge];
3333
}
3434

3535
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType)

React/Modules/RCTUIManager.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey;
8787

8888
/**
8989
* Set the natural size of a view, which is used when no explicit size is set.
90-
* Use UIViewNoIntrinsicMetric to ignore a dimension.
90+
* Use `UIViewNoIntrinsicMetric` to ignore a dimension.
91+
* The `size` must NOT include padding and border.
9192
*/
9293
- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view;
9394

0 commit comments

Comments
 (0)