Skip to content

Commit 145cc56

Browse files
committed
Improve scroll view keyboard insets text handling
1 parent 915be16 commit 145cc56

File tree

5 files changed

+39
-51
lines changed

5 files changed

+39
-51
lines changed

packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.m

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import <React/RCTBridge.h>
1111
#import <React/RCTConvert.h>
1212
#import <React/RCTEventDispatcherProtocol.h>
13+
#import <React/RCTScrollView.h>
1314
#import <React/RCTUIManager.h>
1415
#import <React/RCTUtils.h>
1516
#import <React/UIView+React.h>
@@ -19,6 +20,8 @@
1920
#import <React/RCTTextAttributes.h>
2021
#import <React/RCTTextSelection.h>
2122

23+
#define EMPTY_TEXT_KEYBOARD_BOTTOM_OFFSET 15 // Native iOS empty text field bottom keyboard offset amount
24+
2225
@implementation RCTBaseTextInputView {
2326
__weak RCTBridge *_bridge;
2427
__weak id<RCTEventDispatcherProtocol> _eventDispatcher;
@@ -27,6 +30,31 @@ @implementation RCTBaseTextInputView {
2730
BOOL _didMoveToWindow;
2831
}
2932

33+
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollView *)scrollView
34+
{
35+
if (![self isDescendantOfView:scrollView]) {
36+
// View is outside scroll view
37+
return;
38+
}
39+
40+
UITextRange *selectedTextRange = self.backedTextInputView.selectedTextRange;
41+
UITextSelectionRect *selection = [self.backedTextInputView selectionRectsForRange:selectedTextRange].firstObject;
42+
if (selection == nil) {
43+
// No active selection or caret - fallback to entire input frame
44+
scrollView.firstResponderFocus = [self convertRect:self.frame toView:nil];
45+
return;
46+
}
47+
48+
// Focus on text selection frame
49+
CGRect focusRect = CGRectMake(
50+
selection.rect.origin.x,
51+
selection.rect.origin.y,
52+
selection.rect.size.width,
53+
selection.rect.size.height + (selectedTextRange.empty ? EMPTY_TEXT_KEYBOARD_BOTTOM_OFFSET : 0)
54+
);
55+
scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil];
56+
}
57+
3058
- (instancetype)initWithBridge:(RCTBridge *)bridge
3159
{
3260
RCTAssertParam(bridge);

packages/react-native/React/Views/ScrollView/RCTScrollView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
@property (nonatomic, assign) BOOL snapToEnd;
4949
@property (nonatomic, copy) NSString *snapToAlignment;
5050
@property (nonatomic, assign) BOOL inverted;
51+
@property (nonatomic, assign) CGRect firstResponderFocus;
5152

5253
// NOTE: currently these event props are only declared so we can export the
5354
// event names to JS - we don't call the blocks directly because scroll events

packages/react-native/React/Views/ScrollView/RCTScrollView.m

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
#import "RCTViewUtils.h"
2121
#import "UIView+Private.h"
2222
#import "UIView+React.h"
23-
#import "UIResponder+FirstResponder.h"
2423

2524
/**
2625
* Include a custom scroll view subclass because we want to limit certain
@@ -326,20 +325,17 @@ - (void)_keyboardWillChangeFrame:(NSNotification *)notification
326325
}
327326

328327
CGPoint newContentOffset = _scrollView.contentOffset;
329-
UIResponder *firstResponder = [UIResponder currentFirstResponder];
330-
if ([firstResponder isKindOfClass: [UITextField class]] && [(UITextField *) firstResponder isDescendantOfView:_scrollView]) {
331-
UITextField *textField = [UIResponder currentFirstResponder];
332-
CGRect textFieldFrame = [textField.superview convertRect:textField.frame toView:nil];
333-
CGFloat textFieldBottom = textFieldFrame.origin.y + textFieldFrame.size.height;
334-
CGFloat contentDiff = textFieldBottom - endFrame.origin.y;
335-
if (textFieldBottom > endFrame.origin.y && endFrame.origin.y < beginFrame.origin.y) {
336-
if (self.inverted) {
337-
newContentOffset.y -= contentDiff;
338-
} else {
339-
newContentOffset.y += contentDiff;
340-
}
328+
self.firstResponderFocus = CGRectNull;
329+
330+
if ([[UIApplication sharedApplication] sendAction:@selector(reactUpdateResponderOffsetForScrollView:) to:nil from:self forEvent:nil]) {
331+
// Inner text field focused
332+
CGFloat focusEnd = self.firstResponderFocus.origin.y + self.firstResponderFocus.size.height;
333+
if (focusEnd > endFrame.origin.y) {
334+
// Text field active region is below visible area with keyboard - update offset to bring into view
335+
newContentOffset.y += focusEnd - endFrame.origin.y;
341336
}
342-
} else {
337+
} else if (endFrame.origin.y <= beginFrame.origin.y) {
338+
// Keyboard opened for other reason
343339
CGFloat contentDiff = endFrame.origin.y - beginFrame.origin.y;
344340
if (self.inverted) {
345341
newContentOffset.y += contentDiff;

packages/react-native/React/Views/UIResponder+FirstResponder.h

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/react-native/React/Views/UIResponder+FirstResponder.m

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)