Skip to content

Commit 7b48899

Browse files
sammy-SCfacebook-github-bot
authored andcommitted
Switch order of onSelectionChange and onChange events send from native
Summary: Changelog: [Internal] UIKit uses either `UITextField` or `UITextView` as its UIKit element for `<TextInput>`. `UITextField` is for single line entry, `UITextView` is for multiline entry. There is a problem with order of events when user types a character. In `UITextField` (single line text entry), typing a character first triggers `onChange` event and then `onSelectionChange`. JavaScript depends on this order of events because it uses `mostRecentEventCount` from this even to communicate to native that it is in sync with changes in native. In `UITextView` (multi line text entry), typing a character first triggers `onSelectionChange` and then `onChange`. As JS depends on the correct order of events, this can cause issues. An example would be a TextInput which changes contents based as a result of `onSelectionChange`. Those changes would be ignored as native will throw them away because JavaScript doesn't have the newest version. Reviewed By: JoshuaGross Differential Revision: D20836195 fbshipit-source-id: fbae3b6c0d388fc059ca2541ae980073b8e5f6c7
1 parent 4a48b02 commit 7b48899

File tree

1 file changed

+27
-0
lines changed

1 file changed

+27
-0
lines changed

React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ @implementation RCTTextInputComponentView {
2929
TextInputShadowNode::ConcreteState::Shared _state;
3030
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
3131
size_t _stateRevision;
32+
NSAttributedString *_lastStringStateWasUpdatedWith;
33+
34+
/*
35+
* UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
36+
* entry, UITextView is for multiline entry. There is a problem with order of events when user types a character. In
37+
* UITextField (single line text entry), typing a character first triggers `onChange` event and then
38+
* onSelectionChange. In UITextView (multi line text entry), typing a character first triggers `onSelectionChange` and
39+
* then onChange. JavaScript depends on `onChange` to be called before `onSelectionChange`. This flag keeps state so
40+
* if UITextView is backing text input view, inside `-[RCTTextInputComponentView textInputDidChangeSelection]` we make
41+
* sure to call `onChange` before `onSelectionChange` and ignore next `-[RCTTextInputComponentView
42+
* textInputDidChange]` call.
43+
*/
44+
BOOL _ignoreNextTextInputCall;
3245
}
3346

3447
- (instancetype)initWithFrame:(CGRect)frame
@@ -41,6 +54,7 @@ - (instancetype)initWithFrame:(CGRect)frame
4154
_backedTextInputView = props.traits.multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init];
4255
_backedTextInputView.frame = self.bounds;
4356
_backedTextInputView.textInputDelegate = self;
57+
_ignoreNextTextInputCall = NO;
4458
_stateRevision = State::initialRevisionValue;
4559
[self addSubview:_backedTextInputView];
4660
}
@@ -190,6 +204,8 @@ - (void)prepareForRecycle
190204
_backedTextInputView.attributedText = [[NSAttributedString alloc] init];
191205
_state.reset();
192206
_stateRevision = State::initialRevisionValue;
207+
_lastStringStateWasUpdatedWith = nil;
208+
_ignoreNextTextInputCall = NO;
193209
}
194210

195211
#pragma mark - RCTComponentViewProtocol
@@ -294,6 +310,10 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin
294310

295311
- (void)textInputDidChange
296312
{
313+
if (_ignoreNextTextInputCall) {
314+
_ignoreNextTextInputCall = NO;
315+
return;
316+
}
297317
[self _updateState];
298318

299319
if (_eventEmitter) {
@@ -303,6 +323,12 @@ - (void)textInputDidChange
303323

304324
- (void)textInputDidChangeSelection
305325
{
326+
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
327+
if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
328+
[self textInputDidChange];
329+
_ignoreNextTextInputCall = YES;
330+
}
331+
306332
if (_eventEmitter) {
307333
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSelectionChange([self _textInputMetrics]);
308334
}
@@ -327,6 +353,7 @@ - (void)_updateState
327353
}
328354

329355
auto data = _state->getData();
356+
_lastStringStateWasUpdatedWith = attributedString;
330357
data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString);
331358
_state->updateState(std::move(data), EventPriority::SynchronousUnbatched);
332359
_stateRevision = _state->getRevision() + 1;

0 commit comments

Comments
 (0)