Skip to content

Commit 892212b

Browse files
Mehdi Mulanifacebook-github-bot
Mehdi Mulani
authored andcommittedJul 30, 2018
Fix controlled <TextInput> on iOS when inputting in Chinese/Japanese
Summary: @public This should fix #18403. When the user is inputting in Chinese/Japanese with <TextInput> in a controlled manner, the RCTBaseTextInputView will compare the JS-generated attributed string against the TextInputView attributed string and repeatedly overwrite the TextInputView one. This is because the native TextInputView will provide extra styling to show that some text is provisional. My solution is to do a plain text string comparison at this point, like how we do for dictation. Expected behavior when typing in a language that has "multistage" text input: For instance, in Chinese/Japanese it's common to type out the pronunciation for a word and then choose the appropriate word from above the keyboard. In this model, the "pronunciation" shows up in the text box first and then is replaced with the chosen word. Using the word Japan which is written 日本 but first typed as にほん. It takes 4 key-presses to get to 日本, since に, ほ, ん, are all typed and then 日本 is selected. So here is what should happen: 1. enter に, onChange fires with 'に', markedTextRange covers 'に' 2. enter ほ, onChange fires with 'にほ', markedTextRange covers 'にほ' 3. enter ん, onChange fires with 'にほん', markedTextRange covers 'にほん' 4. user selects 日本 from the menu above the keyboard (provided by the keyboard/OS), onChange fires with '日本', markedTextRange is removed previously we were overwriting the attributed text which would remove the markedTextRange, preventing the user from selecting 日本 from above the keyboard. Cheekily, I've also fixed an issue with secure text entry as it's the same type of problem. Reviewed By: PeteTheHeat Differential Revision: D9002295 fbshipit-source-id: 7304ede055f301dab9ce1ea70f65308f2a4b4a8f
1 parent bda84a3 commit 892212b

File tree

2 files changed

+62
-16
lines changed

2 files changed

+62
-16
lines changed
 

‎Libraries/Text/TextInput/RCTBaseTextInputView.m

+13-5
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,19 @@ - (NSAttributedString *)attributedText
9999
}
100100

101101
- (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{
102-
UITextInputMode *currentInputMode = self.backedTextInputView.textInputMode;
103-
if ([currentInputMode.primaryLanguage isEqualToString:@"dictation"]) {
104-
// When the dictation is running we can't update the attibuted text on the backed up text view
105-
// because setting the attributed string will kill the dictation. This means that we can't impose
106-
// the settings on a dictation.
102+
// When the dictation is running we can't update the attibuted text on the backed up text view
103+
// because setting the attributed string will kill the dictation. This means that we can't impose
104+
// the settings on a dictation.
105+
// Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
106+
// text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc
107+
// for more info.
108+
// Lastly, when entering a password, etc., there will be additional styling on the field as the native text view
109+
// handles showing the last character for a split second.
110+
BOOL shouldFallbackToBareTextComparison =
111+
[self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
112+
self.backedTextInputView.markedTextRange ||
113+
self.backedTextInputView.isSecureTextEntry;
114+
if (shouldFallbackToBareTextComparison) {
107115
return ([newText.string isEqualToString:oldText.string]);
108116
} else {
109117
return ([newText isEqualToAttributedString:oldText]);

‎RNTester/js/TextInputExample.ios.js

+49-11
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,48 @@ class RewriteExampleInvalidCharacters extends React.Component<
173173
}
174174
}
175175

176+
class RewriteExampleKana extends React.Component<$FlowFixMeProps, any> {
177+
constructor(props) {
178+
super(props);
179+
this.state = {text: ''};
180+
}
181+
render() {
182+
return (
183+
<View style={styles.rewriteContainer}>
184+
<TextInput
185+
multiline={false}
186+
onChangeText={text => {
187+
this.setState({text: text.replace(//g, '日')});
188+
}}
189+
style={styles.default}
190+
value={this.state.text}
191+
/>
192+
</View>
193+
);
194+
}
195+
}
196+
197+
class SecureEntryExample extends React.Component<$FlowFixMeProps, any> {
198+
constructor(props) {
199+
super(props);
200+
this.state = {text: ''};
201+
}
202+
render() {
203+
return (
204+
<View>
205+
<TextInput
206+
secureTextEntry={true}
207+
style={styles.default}
208+
defaultValue="abc"
209+
onChangeText={text => this.setState({text})}
210+
value={this.state.text}
211+
/>
212+
<Text>Current text is: {this.state.text}</Text>
213+
</View>
214+
);
215+
}
216+
}
217+
176218
class TokenizedTextExample extends React.Component<$FlowFixMeProps, any> {
177219
constructor(props) {
178220
super(props);
@@ -524,6 +566,12 @@ exports.examples = [
524566
return <RewriteExampleInvalidCharacters />;
525567
},
526568
},
569+
{
570+
title: 'Live Re-Write (ひ -> 日)',
571+
render: function() {
572+
return <RewriteExampleKana />;
573+
},
574+
},
527575
{
528576
title: 'Keyboard Accessory View',
529577
render: function() {
@@ -677,17 +725,7 @@ exports.examples = [
677725
{
678726
title: 'Secure text entry',
679727
render: function() {
680-
return (
681-
<View>
682-
<WithLabel label="true">
683-
<TextInput
684-
secureTextEntry={true}
685-
style={styles.default}
686-
defaultValue="abc"
687-
/>
688-
</WithLabel>
689-
</View>
690-
);
728+
return <SecureEntryExample />;
691729
},
692730
},
693731
{

0 commit comments

Comments
 (0)