From 7b4889937ceb0eccdbb62a610b58525c29928be7 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Thu, 9 Apr 2020 03:41:22 -0700 Subject: [PATCH] Switch order of onSelectionChange and onChange events send from native Summary: Changelog: [Internal] UIKit uses either `UITextField` or `UITextView` as its UIKit element for ``. `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 --- .../TextInput/RCTTextInputComponentView.mm | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index d41fe7cd3fdd79..cdda94fe2d1edc 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -29,6 +29,19 @@ @implementation RCTTextInputComponentView { TextInputShadowNode::ConcreteState::Shared _state; UIView *_backedTextInputView; size_t _stateRevision; + NSAttributedString *_lastStringStateWasUpdatedWith; + + /* + * UIKit uses either UITextField or UITextView as its UIKit element for . 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. In UITextView (multi line text entry), typing a character first triggers `onSelectionChange` and + * then onChange. JavaScript depends on `onChange` to be called before `onSelectionChange`. This flag keeps state so + * if UITextView is backing text input view, inside `-[RCTTextInputComponentView textInputDidChangeSelection]` we make + * sure to call `onChange` before `onSelectionChange` and ignore next `-[RCTTextInputComponentView + * textInputDidChange]` call. + */ + BOOL _ignoreNextTextInputCall; } - (instancetype)initWithFrame:(CGRect)frame @@ -41,6 +54,7 @@ - (instancetype)initWithFrame:(CGRect)frame _backedTextInputView = props.traits.multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init]; _backedTextInputView.frame = self.bounds; _backedTextInputView.textInputDelegate = self; + _ignoreNextTextInputCall = NO; _stateRevision = State::initialRevisionValue; [self addSubview:_backedTextInputView]; } @@ -190,6 +204,8 @@ - (void)prepareForRecycle _backedTextInputView.attributedText = [[NSAttributedString alloc] init]; _state.reset(); _stateRevision = State::initialRevisionValue; + _lastStringStateWasUpdatedWith = nil; + _ignoreNextTextInputCall = NO; } #pragma mark - RCTComponentViewProtocol @@ -294,6 +310,10 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin - (void)textInputDidChange { + if (_ignoreNextTextInputCall) { + _ignoreNextTextInputCall = NO; + return; + } [self _updateState]; if (_eventEmitter) { @@ -303,6 +323,12 @@ - (void)textInputDidChange - (void)textInputDidChangeSelection { + auto const &props = *std::static_pointer_cast(_props); + if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { + [self textInputDidChange]; + _ignoreNextTextInputCall = YES; + } + if (_eventEmitter) { std::static_pointer_cast(_eventEmitter)->onSelectionChange([self _textInputMetrics]); } @@ -327,6 +353,7 @@ - (void)_updateState } auto data = _state->getData(); + _lastStringStateWasUpdatedWith = attributedString; data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString); _state->updateState(std::move(data), EventPriority::SynchronousUnbatched); _stateRevision = _state->getRevision() + 1;