Skip to content

Commit fdfba41

Browse files
authored
feat(textinput): Add a scrollview to multiline TextInput (#2726)
1 parent 55213f8 commit fdfba41

File tree

5 files changed

+231
-2
lines changed

5 files changed

+231
-2
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// [macOS]
9+
10+
#if TARGET_OS_OSX
11+
12+
#import <React/RCTUIKit.h>
13+
14+
#import "RCTTextUIKit.h"
15+
16+
#import <React/RCTBackedTextInputDelegate.h>
17+
#import <React/RCTBackedTextInputViewProtocol.h>
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
@interface RCTWrappedTextView : RCTPlatformView <RCTBackedTextInputViewProtocol>
22+
23+
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
24+
@property (assign) BOOL hideVerticalScrollIndicator;
25+
26+
@end
27+
28+
NS_ASSUME_NONNULL_END
29+
30+
#endif // TARGET_OS_OSX
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// [macOS]
9+
10+
#if TARGET_OS_OSX
11+
12+
#import <React/RCTWrappedTextView.h>
13+
14+
#import <React/RCTUITextView.h>
15+
#import <React/RCTTextAttributes.h>
16+
17+
@implementation RCTWrappedTextView {
18+
RCTUITextView *_forwardingTextView;
19+
RCTUIScrollView *_scrollView;
20+
RCTClipView *_clipView;
21+
}
22+
23+
- (instancetype)initWithFrame:(CGRect)frame
24+
{
25+
if (self = [super initWithFrame:frame]) {
26+
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
27+
28+
self.hideVerticalScrollIndicator = NO;
29+
30+
_scrollView = [[RCTUIScrollView alloc] initWithFrame:self.bounds];
31+
_scrollView.backgroundColor = [RCTUIColor clearColor];
32+
_scrollView.drawsBackground = NO;
33+
_scrollView.borderType = NSNoBorder;
34+
_scrollView.hasHorizontalRuler = NO;
35+
_scrollView.hasVerticalRuler = NO;
36+
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
37+
[_scrollView setHasVerticalScroller:YES];
38+
[_scrollView setHasHorizontalScroller:NO];
39+
40+
_clipView = [[RCTClipView alloc] initWithFrame:_scrollView.bounds];
41+
[_scrollView setContentView:_clipView];
42+
43+
_forwardingTextView = [[RCTUITextView alloc] initWithFrame:_scrollView.bounds];
44+
_forwardingTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
45+
_forwardingTextView.delegate = self;
46+
47+
_forwardingTextView.verticallyResizable = YES;
48+
_forwardingTextView.horizontallyResizable = YES;
49+
_forwardingTextView.textContainer.containerSize = NSMakeSize(FLT_MAX, FLT_MAX);
50+
_forwardingTextView.textContainer.widthTracksTextView = YES;
51+
_forwardingTextView.textInputDelegate = self;
52+
53+
_scrollView.documentView = _forwardingTextView;
54+
_scrollView.contentView.postsBoundsChangedNotifications = YES;
55+
56+
// Enable the focus ring by default
57+
_scrollView.enableFocusRing = YES;
58+
[self addSubview:_scrollView];
59+
60+
// a register for those notifications on the content view.
61+
[[NSNotificationCenter defaultCenter] addObserver:self
62+
selector:@selector(boundsDidChange:)
63+
name:NSViewBoundsDidChangeNotification
64+
object:_scrollView.contentView];
65+
}
66+
67+
return self;
68+
}
69+
70+
- (BOOL)isFlipped
71+
{
72+
return YES;
73+
}
74+
75+
#pragma mark -
76+
#pragma mark Method forwarding to text view
77+
78+
- (void)forwardInvocation:(NSInvocation *)invocation
79+
{
80+
[invocation invokeWithTarget:_forwardingTextView];
81+
}
82+
83+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
84+
{
85+
if ([_forwardingTextView respondsToSelector:selector]) {
86+
return [_forwardingTextView methodSignatureForSelector:selector];
87+
}
88+
89+
return [super methodSignatureForSelector:selector];
90+
}
91+
92+
- (void)boundsDidChange:(NSNotification *)notification
93+
{
94+
}
95+
96+
#pragma mark -
97+
#pragma mark First Responder forwarding
98+
99+
- (NSResponder *)responder
100+
{
101+
return _forwardingTextView;
102+
}
103+
104+
- (BOOL)acceptsFirstResponder
105+
{
106+
return _forwardingTextView.acceptsFirstResponder;
107+
}
108+
109+
- (BOOL)becomeFirstResponder
110+
{
111+
return [_forwardingTextView becomeFirstResponder];
112+
}
113+
114+
- (BOOL)resignFirstResponder
115+
{
116+
return [_forwardingTextView resignFirstResponder];
117+
}
118+
119+
#pragma mark -
120+
#pragma mark Text Input delegate forwarding
121+
122+
- (id<RCTBackedTextInputDelegate>)textInputDelegate
123+
{
124+
return _forwardingTextView.textInputDelegate;
125+
}
126+
127+
- (void)setTextInputDelegate:(id<RCTBackedTextInputDelegate>)textInputDelegate
128+
{
129+
_forwardingTextView.textInputDelegate = textInputDelegate;
130+
}
131+
132+
#pragma mark -
133+
#pragma mark Scrolling control
134+
135+
- (BOOL)scrollEnabled
136+
{
137+
return _scrollView.isScrollEnabled;
138+
}
139+
140+
- (void)setScrollEnabled:(BOOL)scrollEnabled
141+
{
142+
_scrollView.scrollEnabled = scrollEnabled;
143+
[_clipView setConstrainScrolling:!scrollEnabled];
144+
}
145+
146+
- (BOOL)shouldShowVerticalScrollbar
147+
{
148+
// Hide vertical scrollbar if explicity set to NO
149+
if (self.hideVerticalScrollIndicator) {
150+
return NO;
151+
}
152+
153+
// Hide vertical scrollbar if attributed text overflows view
154+
CGSize textViewSize = [_forwardingTextView intrinsicContentSize];
155+
NSClipView *clipView = (NSClipView *)_scrollView.contentView;
156+
if (textViewSize.height > clipView.bounds.size.height) {
157+
return YES;
158+
};
159+
160+
return NO;
161+
}
162+
163+
- (void)textInputDidChange
164+
{
165+
[_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]];
166+
}
167+
168+
- (void)setAttributedText:(NSAttributedString *)attributedText
169+
{
170+
[_forwardingTextView setAttributedText:attributedText];
171+
[_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]];
172+
}
173+
174+
#pragma mark -
175+
#pragma mark Text Container Inset override for NSTextView
176+
177+
// This method is there to match the textContainerInset property on RCTUITextField
178+
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInsets
179+
{
180+
// RCTUITextView has logic in setTextContainerInset[s] to convert the UIEdgeInsets to a valid NSSize struct
181+
_forwardingTextView.textContainerInsets = textContainerInsets;
182+
}
183+
184+
@end
185+
186+
#endif // TARGET_OS_OSX

packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
3838
@property (nonatomic, assign, readonly) BOOL textWasPasted;
3939
#else // [macOS
4040
@property (nonatomic, assign) BOOL textWasPasted;
41+
@property (nonatomic, readonly) NSResponder *responder;
4142
#endif // macOS]
4243
@property (nonatomic, assign, readonly) BOOL dictationRecognizing;
4344
@property (nonatomic, assign) UIEdgeInsets textContainerInset;

packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
217217

218218
#if TARGET_OS_OSX // [macOS
219219

220+
- (NSResponder *)responder
221+
{
222+
return self;
223+
}
224+
220225
+ (Class)cellClass
221226
{
222227
return RCTUITextFieldCell.class;

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
#import <React/RCTUITextField.h>
1717
#import <React/RCTUITextView.h>
1818
#import <React/RCTUtils.h>
19+
#if TARGET_OS_OSX // [macOS
20+
#import <React/RCTWrappedTextView.h>
21+
#endif // macOS]
1922

2023
#import "RCTConversions.h"
2124
#import "RCTTextInputNativeCommands.h"
@@ -86,7 +89,11 @@ - (instancetype)initWithFrame:(CGRect)frame
8689
const auto &defaultProps = TextInputShadowNode::defaultSharedProps();
8790
_props = defaultProps;
8891

92+
#if !TARGET_OS_OSX // [macOS]
8993
_backedTextInputView = defaultProps->multiline ? [RCTUITextView new] : [RCTUITextField new];
94+
#else // [macOS
95+
_backedTextInputView = defaultProps->multiline ? [[RCTWrappedTextView alloc] initWithFrame:self.bounds] : [RCTUITextField new];
96+
#endif // macOS]
9097
_backedTextInputView.textInputDelegate = self;
9198
_ignoreNextTextInputCall = NO;
9299
_comingFromJS = NO;
@@ -672,7 +679,7 @@ - (void)blur
672679
[_backedTextInputView resignFirstResponder];
673680
#else // [macOS
674681
NSWindow *window = [_backedTextInputView window];
675-
if ([window firstResponder] == _backedTextInputView) {
682+
if ([window firstResponder] == _backedTextInputView.responder) {
676683
[window makeFirstResponder:nil];
677684
}
678685
#endif // macOS]
@@ -967,7 +974,7 @@ - (void)_setMultiline:(BOOL)multiline
967974
#if !TARGET_OS_OSX // [macOS]
968975
RCTUIView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new];
969976
#else // [macOS
970-
RCTUITextView<RCTBackedTextInputViewProtocol> *backedTextInputView = [RCTUITextView new];
977+
RCTPlatformView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTWrappedTextView new] : [RCTUITextField new];
971978
#endif // macOS]
972979
backedTextInputView.frame = _backedTextInputView.frame;
973980
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);

0 commit comments

Comments
 (0)